Файловый менеджер - Редактировать - /home/tuudkjt/globeasy/wp-includes/ID3/woocommerce-payments.tar
Назад
readme.txt 0000777 00000756534 15251156605 0006603 0 ustar 00 === WooPayments: Integrated WooCommerce Payments === Contributors: woocommerce, automattic Tags: woocommerce payments, apple pay, credit card, google pay, payment, payment gateway Requires at least: 6.0 Tested up to: 6.9 Requires PHP: 7.3 Stable tag: 10.5.1 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html Securely accept credit and debit cards on your WooCommerce store. Manage payments without leaving your WordPress dashboard. Only with WooPayments. == Description == **Payments made simple, with no monthly fees – designed exclusively for WooCommerce stores.** Securely accept major credit and debit cards, and allow customers to pay you directly without leaving your WooCommerce store. View and manage transactions from one convenient place – your WordPress dashboard. See payments, track cash flow into your bank account, manage refunds, and stay on top of disputes without the hassle of having to log into a separate payment processor. **Manage transactions from the comfort of your store** Features previously only available on your payment provider’s website are now part of your store’s **integrated payments dashboard**. This enables you to: - View the details of [payments, refunds, and other transactions](https://woocommerce.com/document/woopayments/managing-money/). - View and respond to [disputes and chargebacks](https://woocommerce.com/document/woopayments/fraud-and-disputes/managing-disputes/). - [Track payouts](https://woocommerce.com/document/woopayments/payouts/) into your bank account or debit card. **Pay as you go** WooPayments is **free to install**, with **no setup fees or monthly fees**. Our pay-as-you-go pricing model means we're incentivized to help you succeed! [Read more about transaction fees](https://woocommerce.com/document/woopayments/fees/). **Supported by the WooCommerce team** Our global support team is available to answer questions you may have about WooPayments installation, setup, or use. For assistance, [open a ticket on woocommerce.com](https://woocommerce.com/my-account/contact-support/?select=5278104). == Getting Started == = Requirements = * WordPress 6.0 or newer. * WooCommerce 7.6 or newer. * PHP 7.3 or newer. = Try it now = To try WooPayments (previously WooCommerce Payments) on your store, simply [install it](https://wordpress.org/plugins/woocommerce-payments/#installation) and follow the prompts. Please see our [Startup Guide](https://woocommerce.com/document/woopayments/startup-guide/) for a full walkthrough of the process. == Installation == Install and activate the WooCommerce and WooPayments plugins, if you haven't already done so, then go to "Payments" in the WordPress admin menu and follow the instructions there. == Frequently Asked Questions == = What countries and currencies are supported? = If you are an individual or business based in [one of these countries](https://woocommerce.com/document/woopayments/compatibility/countries/#supported-countries), you can sign-up with WooPayments. After completing sign up, you can accept payments from customers anywhere in the world, as long as they are paying with [a supported currency](https://woocommerce.com/document/woopayments/currencies/available-currencies/). We are actively planning to expand into additional countries based on your interest. Let us know where you would like to [see WooPayments launch next](https://woocommerce.com/payments/#request-invite). = Why is a WordPress.com account and connection required? = WooPayments uses the WordPress.com connection to securely communicate with our payments server. This is necessary for WooPayments to function correctly, so it's not possible to use WooPayments without connecting. Please see [this document](https://woocommerce.com/document/woopayments/our-policies/connection/) for more information. Note that you do not need to host your site itself on WordPress.com, nor do you need to host *any* site on WordPress.com at all. You only need an account. Note that you do not need to host your site itself on WordPress.com. You only need an account on WordPress.com to establish the connection. You can create a WordPress.com account [at this link](https://wordpress.com/start/account/user-social). = How do I set up a store for a client? = If you are a developer or agency setting up a site for a client, please see [this page](https://woocommerce.com/document/woopayments/account-management/developer-or-agency-setup/) of our documentation for some tips on how to install WooPayments on client sites. = How is WooPayments related to Stripe? = WooPayments is built in partnership with Stripe [Stripe](https://stripe.com/). When you sign up for WooPayments, your personal and business information is verified with Stripe and stored in an account connected to the WooPayments service. This account is then used in the background for managing your business account information and activity via WooPayments. [Learn more](https://woocommerce.com/document/woopayments/account-management/partnership-with-stripe/). = Are there Terms of Service and data usage policies? = You can read our Terms of Service and other policies [here](https://woocommerce.com/document/woopayments/our-policies/). == Screenshots == 1. View Transactions 2. View Transaction Details 3. Track Payouts 4. Manage Disputes == Changelog == = 10.5.1 - 2026-02-11 = * Fix - Cache the affected orders check for the canceled auth fee remediation note to avoid an expensive query on every admin page load = 10.5.0 - 2026-02-05 = * Add - Add "Other" and "Booking/Reservation" product type support for dispute evidence (feature flag gated) * Add - Add ability to specify preferred communications email. * Add - Add evidence matrix entry for subscription_canceled + booking_reservation dispute combination * Add - Add remediation tool to fix incorrect analytics data from canceled authorizations * Add - feat: add Amazon Pay settings storage * Add - feat: add confirmation tokens feature flag * Add - feat: Amazon Pay ECE button front-end implementation * Add - Show an informative notice when a previously eligible merchant loses instant payout eligibility, instead of silently hiding the button. * Fix - Allow subscription renewals with saved Link payment tokens even when Link is disabled at checkout * Fix - fix: payment method title displayed on page editor for block-based checkout page * Fix - fix: subscription behavior with confirmation tokens * Fix - fix: text wrap of google pay/apple pay descriptions * Fix - Fix E2E test for non-admin WP-Admin access to expect 'Connected' instead of 'Complete' account status. * Fix - Fixed fatal error in email preview when viewing third-party plugin emails * Fix - Fixed iDEAL logo sizing in checkout to match other payment method logos. * Fix - Fix exit survey option not being preserved due to string type validation failure. * Fix - Fix migration log file cleanup compatibility with WooCommerce 8.6+. * Fix - Fix REST schema type for stripe_billing_subscription_count and stripe_billing_migrated_count from 'int' to 'integer' * Fix - Fix the case of double exchange rate applied to the displayed fee amount in some scenarios. * Fix - Fix the logic of the WooPay button on single product pages. * Fix - Improved subscription migration log file merging to use stream-based operations for memory efficiency and preserve chronological order. * Fix - Make sure that nonces are loaded correctly for the Add Payment Method page. * Fix - Prevent fatal error on sites running WooCommerce Subscriptions < 5.0.0 by not instantiating the Stripe Billing migrator when required functions are unavailable. * Fix - Update AVS Mismatch fraud filter copy to remove street check * Fix - Update dispute evidence fields to match specification document * Update - Bump the minimum required PHP version to 7.4 * Update - chore: update Alipay/WeChat Pay settings descriptions to state restrictions with regional versions * Update - Improved and simplified Visa Compliance dispute screen. * Update - refactor: ECE to use confirmation tokens instead of payment methods * Update - Tested up to WooCommerce 10.5.0 * Update - update: allow server-side control of confirmation token logic * Update - Use badge component in 'Respond by' column of the Disputes table for consistency with the 'Response needed' status badge * Dev - Add error logging for embedded KYC session failures to improve debugging. * Dev - Add non-interactive changelog command for automation * Dev - Add QIT E2E workflows for PR, scheduled, and pre-release testing with L-1 WooCommerce version support. * Dev - Add WooCommerce core reference instructions to Claude Code configuration * Dev - Bump the version of WP in tested up to readme * Dev - chore: removed unused entries from the `wcpay_payment_fields_js_config` filter; ensured single call of the `wcpay_payment_fields_js_config` filter; * Dev - chore: remove unused ECE utilities * Dev - Clean up after rolling out the new Account Details in the Overview page. * Dev - Migrate subscription E2E tests to QIT test-package infrastructure. * Dev - refactor: scoping of UPE utilities at checkout * Dev - Remove flaky E2E admin analytics test in favor of QIT coverage * Dev - Stop using dynamic keys for the database cache and move cached payment methods to user meta. * Dev - [E2E][QIT] Migrate shopper subscription E2E test specs to QIT test-package = 10.4.0 - 2026-01-07 = * Add - Add duplicate charge documentation field for "Not Duplicate" dispute scenario. * Add - Adding ability to support payment method promotional campaigns (spotlight and badge treatments). * Add - feat: add Amazon Pay payment gateway class * Add - Open up new Account Details in the Overview page by enabling the feature flag by default * Add - When editing subscriptions, load payment methods whenever the customer is changed. * Fix - Add order note when WooPay checkout fails. * Fix - Attempt to fix when items remain in the cart after successful checkout (Affirm). * Fix - Avoid race conditions while capturing payments. * Fix - Comment: Update links to the WooPayments fees documentation. * Fix - fix: ensuring that the Express Checkout Buttons show the correct amounts for currencies formatted in unusual ways (e.g.: USD with zero decimals) * Fix - fix: order attribution data on block-based checkout * Fix - fix: payment method definitions merge conflicts * Fix - fix: payment method logos compatibility with WooCommerce Blocks in WC>=10.4 * Fix - fix: redirect message styles on block-based checkout page * Fix - Fix: Stripe Link availability on checkout outside of USD * Fix - fix: VAT error messages localization * Fix - fix: when WooCommerce Core sends the email about the activated payment method, redirect the merchant to the main settings page instead of a non-existent page. * Fix - Fix number format when converting order prices. * Fix - Implemented amount mismatch detection for an already paid order. * Fix - refactor: remove Stripe Link autofill modal button from checkout * Fix - Resolve JavaScript errors in WooPay Direct Checkout on sites with WooCommerce 10.4+ * Fix - Respect merchant tracking preferences for shopper events * Fix - Update wording related to the Visa Compliance disputes. * Update - chore: remove unused `wcBlocksCheckoutData` constant * Update - chore: update account fees docs URL * Update - Improve how server-side and client-side check tracking usage eligibility. * Update - refactor: convert Klarna payment method into a payment method definition * Update - refactor: Google Pay/Apple Pay settings storage consistency * Update - refactor: remove Afterpay_Payment_Method in favor of AfterpayDefinition * Update - refactor: remove payment_request_enabled_locations and woopay_enabled_locations in favor of a more future-proof express_checkout_product_methods express_checkout_cart_methods express_checkout_checkout_methods * Update - refactor: update payment method definition classes to delegate capabilities to PaymentMethodUtils * Update - update: bring back card borders on settings screens. * Update - update: payment method definition to determine eligible countries * Update - update: remove Google Pay test mode notice with live account, it's no longer necessary * Update - update: usage of WP best practices * Update - Update dispute evidence fields and cover letter for Booking/Reservation duplicate disputes * Update - Update suggested evidence for the Subscription Cancelled dispute reason. * Update - Update wording on promo rate information tooltip * Update - Update “WooPayments” to “WooCommerce” mobile application in the Card Readers page. * Dev - Add validation to prevent Request classes from using empty hook names, which could cause fatal errors on PHP 8.0+ * Dev - Bump WC tested up to version to 10.4.0 * Dev - chore: remove CC_Payment_Gateway class in favor of WC_Payment_Gateway_WCPay * Dev - Cleanup unneeded asset. * Dev - Fix failing E2E tests * Dev - Log exceptions during the payment process. * Dev - Migrate remaining shopper E2E specs to QIT (my account, multicurrency, alternative payment methods) * Dev - refactor: add Apple Pay and Google Pay payment definitions * Dev - refactor: migrate remaining non-card payment methods into payment method definitions * Dev - refactor: updating some payment methods to use payment definitions instead * Dev - Update Jetpack packages. = 10.3.1 - 2025-12-22 = * Fix - Fix fatal error on WooCommerce Settings Payments tab caused by missing hook in Get_PM_Promotions request class = 10.3.0 - 2025-12-10 = * Add - Add backend support for additional dispute evidence types (event, booking, other) behind feature flag. * Add - Adding ability to support payment method promotional campaigns (spotlight and badge treatments). * Add - Allow non-reusable payment methods to be used for the manually renewed subscriptions. * Add - chore: add amazon pay feature flag. * Add - Handling of the Visa Compliance disputes with attention to a specific dispute fee. * Fix - Comment: Update links to the WooPayments fees documentation. * Fix - fix: ensuring that the Express Checkout Buttons show the correct amounts for currencies formatted in unusual ways (e.g.: USD with zero decimals) * Fix - fix: payment method logos compatibility with WooCommerce Blocks in WC>=10.4 * Fix - fix: text color of payment method icons on checkout page when a dark background is used * Fix - Fix deprecation warning about usage of `parent` in callables. * Fix - Fix styling of the WooPay button to make sure that the spinner is visible when loading. * Fix - Fix WooPay express button text clipping * Fix - Refunds and fees should not be tracked for canceled authorizations * Fix - WooPay component spacing issues on blocks and classic checkout. * Update - Change payout texts for New Account Waiting Period to be consistent with new Account Details * Update - chore: update account fees docs URL * Update - Update “WooPayments” to “WooCommerce” mobile application in the Card Readers page. = 10.2.0 - 2025-11-06 = * Add - Add WooCommerce Tool to delete test orders. * Add - Sync store setup details with the Transact Platform. * Fix - Comment: fix JCB logo. * Fix - Fix - WCPay Subscriptions setting not persisting when unchecked * Fix - Fix missing payment method logos (JCB, CUP, CB) in checkout. * Fix - Fix payment method logos overflow in shortcode checkout after adding JCB and UnionPay logos. * Fix - Only show WooPay error messages for connection errors in available countries. * Fix - Prevent Express Checkout from being disabled on product page when updating quantity of variable products. * Fix - Prevent WooPayments-specific styles for the Checkout block to leak to other payment methods * Fix - Use Woo custom cap `pay_for_order` for validating the owner of order * Update - Avoid loading shipping zones when adjusting currencies for free shipping methods. * Update - Hide bundled subscription management UI while preserving renewal processing functionality * Update - Minor copy update to the delete test orders tool. * Update - Show message about bundled subscriptions until 10.2.0. * Dev - Add CLAUDE.md file to improve AI code assistance. * Dev - Bump WC tested up to version to 10.3.0. = 10.1.1 - 2025-10-27 = * Fix - Overrides the core `AbstractAutomatticAddressProvider`'s `load_scripts` method to only run if the core setting (`woocommerce_address_autocomplete_enabled` is enabled). = 10.1.0 - 2025-10-15 = * Add - Disputes - Add a loading state to the “Challenge dispute” flow * Add - Pre-fill product type in dispute evidence form based on order products. * Fix - Add caching to address autocomplete token * Fix - Fix edge case when shipping information is added by mistake for digital products. * Fix - Fix _load_textdomain_just_in_time error when loading a non-existing payment method * Fix - Handle dispute counts for menu items between live and test mode * Fix - Single use coupons issues on WooPay. * Fix - Use Woo custom cap `pay_for_order` for validating the owner of order * Update - Show message about bundled subscriptions until 10.2.0. * Dev - Comment: Skip SSL verification when running the PHP tests locally. * Dev - Removed clear_cache method from the mutli-currency interface = 10.0.0 - 2025-09-24 = * Add - Add new feature flag for the dynamic checkout place order button, and new option on payment settings. * Add - New AccountDetails: Add feature flag, TypeScript definition, and basic component * Fix - Do not ask for shipping info for digital products dispute. * Fix - Fix Google/Apple Pay "State / County is required" error during checkout for Saudi Arabia * Fix - Fix possible errors when WooPay is enabled while the store is in Coming Soon mode * Fix - Name is required for the Affirm payment method. * Update - Improve messaging around test accounts and development mode on the Payments Overview page. * Update - Introduce account.deleted webhook processing for a smoother experience when an account is deleted from the Transact Platform. * Dev - Bump WC tested up to version to 10.2.0 * Dev - Fix flaky E2E test in addSavedCard function = 9.9.0 - 2025-09-03 = * Fix - Conditionally set Stripe Link wallet option based on payment method configuration * Fix - fix: adding some missing i18n wrappers * Fix - Fix checks for the billing details for the BNPL methods on the Pay for Order page. * Fix - Fixed WooPay terms and conditions text for merchants using blocks checkout. * Fix - Fix margins for phone number input and add styling to match other inputs * Fix - Fix VAT setup modal for unsupported merchants * Fix - Generate payment method details in WooPayments instead of Woo core, cache them for performance improvements. * Fix - Ignore webhooks whenever the order key in their body does not match the local order. * Fix - Show Activate payments notice in WooPayments Settings only for test accounts. * Update - update: label text on the support phone number in test mode * Update - update: settings page design audit. * Update - Update WP components for development and tests. * Update - Update “Finish setting up WooPayments” task to redirect to NOX flow. * Dev - Fix E2E subcription shopper test failures because the core changes text from "Sign up now" to "Add to cart" * Dev - Update JS packages mini-css-extract-plugin and shelljs = 9.8.0 - 2025-08-13 = * Add - Add address autocomplete to Checkout * Add - Add support for small screens on the evidence submission form. * Fix - Fix customer details section when name and/or email are very big. * Fix - Fix Google/Apple Pay "State / County is required" error during checkout for Croatia * Fix - Fix the icon border of the Steps component for a dispute. * Fix - Reset the enabled payment methods to default value on account reset. * Fix - Suppress sending completed-renewal-order email after dispute resolution * Update - As of this change, we stop bundling WordPress Components in favor of using the wp.components available in the WordPress installation. * Update - Make onboading pages use the pw.components available in the WordPress installation. * Update - Make the connect acctount components use the wp.components from the installation. * Update - Make the KYC onboarding use the WP components available in the installation. * Update - update: ensure Google Pay/Apple Pay can check out w/ LT addresses * Update - update: fraud protection rules to use WP components bundled within the WP installation * Update - Update documents page to use WP components available in the WordPress installation. * Update - Update loan page to use the installation WordPress components. * Update - Update the "Learn more about disputes" link in the confirmation screen of the disputes documentation. * Dev - Fix: enhances the robustness of the selectPaymentMethod method in end-to-end tests * Dev - Fix: Respond to a dispute e2e tests * Dev - Fix: select payment method shopper util * Dev - Removed all the Progressive Onboarding (PO) code. * Dev - update: use React 18 * Dev - Update tested WooCommerce version to 10.1.0 = 9.7.0 - 2025-07-24 = * Add - Add a confirmation screen to the new flow for submitting dispute evidence. * Add - Add Credit not processed logic for the new evidence submission form. * Add - Add external help link to the recomended documents section. * Add - Add logic for Duplicate chargeback reason for the new evidence submission form. * Add - Add logic for subscription cancelled for the new evidence form. * Add - Add new transaction type fee_refund * Add - File name should show size the extension * Add - New evidence flow for the "General" dispute type. * Fix - Accept TAP_TO_PAY_DEVICE as a valid card reader model to consider a transaction as TTP. * Fix - Adjusted deactivation modal behavior to correctly resize when its content updates. * Fix - Change copy to avoid leaving words alone. * Fix - Consolidate test account notices in one in the WooPayments Settings page. * Fix - Enable WooPay Direct Checkout on mini-cart. * Fix - Fix - Remove unwanted animations and improve styling consistency on dispute evidence pages. * Fix - Fix: Allow multiple authorization cancellations on the same order. * Fix - Fix: Google Pay 3D Secure authentication being bypassed due to redirect URL extraction issue * Fix - fix: remove BNPLs from PMME when BNPL method is not active * Fix - Fix: Spinner alignment issues in cart checkout button and checkout address form email field. * Fix - Fix border color of some items. * Fix - Fix copy for the documents section and fix the font size of the content inside the accordion on top. * Fix - Fix filename issue on the new evidence submission form. * Fix - Fix margin of the shipping details title on the evidence submission form. * Fix - Fix order notes after subscriptions' payment method gets changed. * Fix - Fix spacings on the envidence submission form * Fix - Fix vertical UI shift when uploading a file * Fix - Fix WooPay button spinner on some themes. * Fix - Improve the cover letter save behavior. * Fix - Provide correct language code when requesting CSV exports in a region-specific language * Fix - Remove unnecessary gap * Fix - Step back should also scroll the page to the top * Fix - Support numbers starting from 4, 7, 8 for Hong Kong phones. * Update - Disallow challenging the Visa compliance disputes. * Update - ensure payments and transactions pages uses wp components that come with the WP installation. * Update - Improve multicurrency caching and use new API endpoint. * Update - Make the card reaaders page use the wp.components available in the WordPress installation. * Update - Redirect merchant to the dispute detail screen * Update - Redirects to the dispute list after save. * Update - Removes the feature flag for the new evidence submission form. * Update - update: deactivation survey to use WP components bundled within the WP installation * Update - update: ensure multi-currency settings page uses wp components that come with the WP installation; updated some styles to use WP components, rather than bespoke components; * Update - update: ensure settings page uses wp components that come with the WP installation * Update - update: multi-currency widget to use WP components bundled within the WP installation * Update - update: order page assets with wp components * Update - update: RedirectToTransactionDetails to use Gutenberg components that come with the WP installation * Update - update: removed the payment gateways confirmation dialog * Update - update: subscription-product-onboarding to use WP components bundled within the WP installation * Update - update: TOS agreement dialog to use WP Components bundled within the WP installation * Update - Update client to use NOX surfaces instead of MOX/Connect page wherever possible. * Update - Update copy for disputes * Update - Update Multi-currency settings to use wp.components available in the WordPress installation. * Update - Update the payments overview page to use the components available in the WP installation. * Update - Update the payout list and payout details pages to use the WP components available on the WP installation. * Dev - Fix imports for the unbundled components. * Dev - Prevent "Evidence saved" toast from appearing multiple times. * Dev - Remove conversion to the server locale, as it is done on the server. * Dev - Remove feature flag _wcpay_feature_documents * Dev - Remove old evidence submission code * Dev - update: ensure `wp-components-wrapped` has individually exported components = 9.6.0 - 2025-07-02 = * Add - Add cover letter for the new evidence submission screen for Product Unacceptable chargeback reason * Add - Added a URL parameter to allow merchants to access the VAT details modal. * Add - Show an admin notice when bundled subscriptions are in use * Fix - Add NO VAT description for Norway in tax descriptions * Fix - fix: ensure Google Pay/Apple Pay postcode matching for shipping zones works even when the provided postcode is redacted. * Fix - Fix a layout shift when uploading evidence for a dispute on the new challenge dispute screen * Fix - Fix mobile version of the StepperPanel and make the top navigation clickable. * Fix - Fix padding on the recommended document section * Fix - Fix payment methods logos width calculation in block editor preview * Fix - Fix preselected Payment Methods ignored during account creation via KYC session. * Fix - Fix spacing on disputes expandable block * Fix - Fix state for the cover letter. * Fix - Fix the browser print preview page on the payouts page. * Fix - Prevent double stock reduction after (3DS) authentication. * Fix - Prevent fatal error on the Pay for Order page upon deleted orders. * Update - Change the copy of the cover letter notice. * Update - Clicking on the Next button scroll the page position to the top. * Update - Evidence submission form for product not received * Update - fix: update Google Pay/Apple Pay buttons compatibility when multi-currency functionality is disabled - they'll no longer throw an exception for logged-out customers * Update - Refine "Temporarily Suspended" language, statuses and tooltip copy * Update - update: add `wcpay_express_checkout_js_params` filter for express checkout JS params. * Update - Updates the recomended documents section for the new disputes experience * Dev - Add missing arrow icons from the new evidence form * Dev - Ensure the submitted state is working as expected * Dev - Fix the regexp checking for the loading state of the checkout button to cover changes in latest WooCommerce. * Dev - Remove CTA from the new evidence "Evidence saved!" toast. * Dev - Revert @woocommerce/onboarding upgrade in PR 10840 * Dev - Update reviewers for dependabot * Dev - Update tested WooCommerce version to 10.0.0 = 9.5.0 - 2025-06-11 = * Add - Add specific texts for collecting Norway and New Zealand tax data * Add - Add specific texts for collecting Singapore tax data * Add - Add tax to the fee breakdown on order notes * Add - Enable Australian merchants to add tax details (ABN) for generating tax documents. * Add - New Filter: wcpay_woopay_enabled – Allows developers to conditionally disable WooPay based on custom logic, improving compatibility with themes and plugins. * Fix - fix: ensure `checkPaymentMethodIsAvailable` cache key is correctly memoized on express checkout elements * Fix - fix: formatting of currencies in the backend will always honor the merchant's settings, rather than the currency locale. * Fix - fix: set the correct payment method title when processing the payment with wechat pay, multibanco, and others * Fix - Fixed handling of orders containing items with negative tax * Fix - Handle state field requirements for Express Checkout in countries without states * Fix - Prevent accounts scheduled for deletion from being accessible to the WooPayments client to avoid exposing stale or partial data. * Update - Evidence submission form now should use the global wp.components * Update - refactor: add data massaging from legacy google pay/apple pay for HK-based addresses * Update - Refactor: Remove unused Props interface from AccountTools component * Update - Remove WooPayments adding their sections into other gateways when enabled * Update - update: Apple Pay domain verification file is no longer needed * Update - update: remove `_wcpay_feature_tokenized_cart_ece` feature flag; remove `client/express-checkout` in favor of `client/tokenized-express-checkout` * Dev - Add cover letter for fraudulent reason * Dev - Add the components that will be used on the evidence form * Dev - Add the logic "Save for later" when submitting evidence * Dev - Add the new StepperPanel component. * Dev - Bump WC tested up to version to 9.8.5 * Dev - chore: update @stripe/connect-js @stripe/react-connect-js @stripe/react-stripe-js @stripe/stripe-js dependencies * Dev - Clean up database options after completing onboarding. * Dev - Condition AliPay block tests under shouldRunWCBlocksTests * Dev - Create a new feature flag for the evidence submission form * Dev - Decouple the accordion component from the dispute details screen. * Dev - fix: is_store_api_request utility on multisite with subdirectory install * Dev - refactor: introduced MSW for mocking network requests in tests. = 9.4.0 - 2025-05-21 = * Add - Add support for rendering fee tax details in the transaction timeline where available * Fix - Keep the shipping costs structure intact when applying multi-currency adjustments. * Fix - Only display the transaction URL for authorized intent statuses in WP Admin order page * Fix - Optimize session handling by only setting cookies on cart and checkout pages, improving product page caching performance. * Update - chore: remove "add payment methods" wizard task in favor of NOX flow. * Update - chore: update "Tested up to" WordPress 6.8 * Update - Improve error message for embedded onboarding. * Update - Improve the user experience of the WooCommerce Payments dispute management interface, focusing on visual clarity of the dispute steps component that guides merchants through the process of handling payment disputes and inquiries. * Update - Perist payment method checkbox after capability request * Update - refactor: adjust payment method settings UI to show inline notices and disabled checkboxes instead of `(!)` icon * Update - Remove self-assessment fields from the onboarding flow * Update - update: ensure Klarna is never part of ECE buttons * Update - update: remove BNPL payment method messaging element/offering from checkout labels * Update - Update ‘Activate payments’ task to use NOX flow when the reactify-settings-payments feature is enabled. * Dev - refactor: created a "common" component for the payment method details on the transactions view, made for Affirm, Afterpay, Alipay, WeChat Pay, Grabpay, Multibanco * Dev - Set payment method title for order before order status change to make it visible for status change actions. * Dev - Unbundles wordpress/components for Dispute screens * Dev - upgrade currency-switcher block to apiVersion 3 = 9.3.0 - 2025-05-05 = * Add - Add dedicated onboarding REST API endpoint for disabling test drive account, when possible. * Add - Transaction Fees breakdown component in the Payment details. * Fix - Disabled the Transactions Breakdown section to avoid misleading data in certain scenarios. * Fix - fix: ensuring that Google Pay/Apple Pay buttons hide on shortcode cart & checkout when totals go to 0 * Fix - fix: Multibanco payment instructions font size adjustment on some block-based themes (e.g.: Twenty-Twenty-Four, Twenty-Twenty-Three) * Fix - Fix a bug when the notice after downloading CSV that was mispositioned. * Fix - Fix capabilities query param type * Fix - Fixed inconsistent spacing between "Add to Cart" button and express checkout buttons on product pages. * Fix - Fixes a styling issue when the payment method has a tooltip next to it, it was shifting the logo to the right. * Fix - Fix for validation issue with POST params in some cases generating account session. * Fix - Improve styling of the Embedded components to be closer to WPDS. * Fix - Improve subscriptions code compatibility to avoid causing fatal errors. * Fix - Remove the referrer check to update the fraud protection settings * Fix - Set the onboarding test mode while creating test drive account with API * Update - Add dedicated onboarding REST API endpoint for resetting onboarding, when possible. * Update - Advanced fraud protection settings redesign. * Update - chore: removed notices about the JCB capability request. JCB will be automatically requested for every new and existing merchant, regardless of the merchant country. * Update - Include a failure message in the order notes when Stripe Billing subscription renewal has failed. * Update - On the payment settings page, change the "Credit/Debit Cards" icon to a more generic icon and add a static list of card brands below the "Credit/Debit Cards" element. * Update - Remove progressive onboarding eligibility check during embedded KYC session creation * Update - Simplified refund handling with clear errors and standard reasons to aid resolution. * Update - Updated the Stripe locales list. * Update - Update log file format for better compatibility with the WooCommerce log viewer. * Dev - Add Cursor config folder to .gitignore * Dev - Bump WC tested up to version to 9.8.1 * Dev - Bump WC tested up to version to 9.8.2 * Dev - Delete NOX profile during account reset from Overview or Connect page. * Dev - Fix Apple Pay and Google Pay not requested from NOX In-context flow. * Dev - Merged WC_REST_Payments_Payment_Intents_Create_Controller back into WC_REST_Payments_Payment_Intents_Controller after confirming that the issue that caused the split was solved. * Dev - Replace WCPay in messages with WooPayments = 9.2.1 - 2025-04-23 = * Update - Update account session creation route definition to use POST rather than GET. = 9.2.0 - 2025-04-09 = * Add - Add back button for tertiary+ level pages in WooPayments settings. * Fix - fix: cancel GooglePay/ApplePay dialog on product page if add-to-cart product validation fails * Fix - fix: fatal error when Klarna is enabled on an EU account and a non-EU currency is configured on the store. * Fix - fix: Google Pay/Apple Pay display on pay-for-order pages. * Fix - Fix deprecated hook woocommerce_rest_api_option_permissions * Fix - Fix errors in WooCommerce email settings preview * Fix - Fix Multi-currency conversion for WooCommerce Bookings range type cost adjustments * Fix - Fix PMME display on shortcode cart with block-based themes. * Fix - Fix WooPay enabled during NOX onboarding despite being disabled in recommended payment methods. * Fix - Handle pending refunds properly * Fix - Linked account ID to product ID to maintain consistency and prevent issues when the account ID changes. * Fix - Prevent unsaved changes dialog when changes have been saved. * Fix - Removed hard-coded lists of payment methods where possible. * Fix - Remove unused wcpay_date_format_notice_dismissed option from the permission list * Fix - Set background color to white for the Payments settings page * Fix - update: ensure Google Pay/Apple Pay honor 'Display prices during cart and checkout' setting * Fix - Update WooPay icon on order page. * Update - Added _wcpay_net to the metadata. * Update - Chore: check the array type in dismissed noticeces component. * Update - chore: disable request of JCB capability * Update - fix: Google Pay/Apple Pay HK test address override. * Update - fix: parsing of error message for GooglePay/ApplePay buttons to be displayed to customer, instead of displaying generic error message on failure. * Update - Improve the ECE container loading experience. * Update - Move payment method map definition to the backend * Update - Prevent creation of the renewal orders if original order was created in the different WooPayments mode. * Update - refactor: delete temporary Google Pay/Apple Pay cart contents right after making the request, to improve performance and avoiding bots sending wrong session data in subsequent requests. * Update - Remove fraud protection discoverability and update tour * Update - Stripe Billing and Manual Capture incompatibility notice on the Settings page. * Update - Update Settings page as per the new design * Update - Update to Cash App Afterpay branding. * Dev - Bump WC tested up to version to 9.7.1. * Dev - Fix unneeded double square brackets in the post-merge script * Dev - Removed the deprecated wcpay_exit_survey_dismissed option from the ALLOWED_OPTIONS list. * Dev - Remove level3 retry logic and legacy request_with_level3_data method * Dev - Updated the progressive parameter in the KYC session creation API to use a boolean type. * Dev - We switch to using site instead of url as the key in the self assessment data to avoid XSS firewall false-positives. = 9.1.0 - 2025-03-19 = * Add - Add a prompt to encourage merchants to provide feedback or leave a WordPress.org review for WooPayments * Add - Add order failure message on Order Received page. * Add - Add support to change payment method to 3DS card for subscriptions. * Add - Add WeChat Pay response handling. * Add - Implement Multibanco payment method * Add - Implement specific handling for insufficient_balance_for_refund when refunding the admin order management * Add - Inform Stripe when a store switch from coming soon to live mode * Add - Show failure reason in details page for failed payout. This will help merchants get better support, or understand the next steps needed to fix the failing payouts. * Fix - Add an order note when a recurring payment fails or when updating the payment method fails due to a missing or invalid payment token. * Fix - Bypass enabled at checkout checks for rendering payment methods configs. * Fix - Correct the dispute notice for Klarna Inquiries * Fix - fix: GooglePay/ApplePay fail when there are more than 9 shipping options. * Fix - fix: GooglePay/ApplePay script dependencies with WooCommerce 9.7 * Fix - Fix issue where survey modal is not scrollable on smaller screen sizes. * Fix - Fix validation for support phone numbers for Singapore * Fix - Improve payout failure messages for better clarity and accuracy * Fix - Inconsitent Safe Mode notice with the latest Jetpack version * Fix - Init PMME container in cart block so that it can be dynamically rendered once the requirements are met. * Fix - Make sure that WooPayments gateways follow the main WooPayments card gateway in gateway ordering on the page. * Fix - Manual capture fails in the transaction detail screen with a customized order number * Fix - Properly extract styles when using the site editor. * Fix - Renamed function parameters to avoid reserved keyword conflicts * Fix - Resolved an issue on stores that had the Stripe Billing feature enabled (US-only) and then changed their store location to an ineligible country. * Fix - Scoped CSS selectors for WP components to prevent unintended styling on other pages * Fix - Show Express Checkout button previews in template editor * Fix - Skip email input search in pay for order flow and use email provided in order data for WooPay iframe. * Fix - Skip limits per currency check on admin pages * Fix - Tests: Suppressed unexpected JSON output in maybe_handle_onboarding test by wrapping execution with ob_start() and ob_end_clean() * Update - Better handling of HTTPs errors in embedded components. * Update - Change wording for Sales Channel, Online Store, In-Person, and In-Person (POS) * Update - Enhancements to country select field in onboarding. * Update - feat: add compatibility notice for Google Pay with live mode accounts. * Update - Jetpack packages in composer * Update - Remove the 60 day survey admin note, since it will be redundant after we add the reviews prompt. * Update - Track action complete event in Stripe Notification embedded component. * Update - update: tokenize ECE initialization and update flow on pricing change. * Dev - Add centralized payment method definitions to streamline implementation and maintenance of payment methods. * Dev - Exclude playwright-report from eslint. * Dev - Include transaction ID when requesting card reader fee charges summary. * Dev - Refactors to the embedded compoennts logic. * Dev - Update @wordpress/icons version to latest version 10.19.0 = 9.0.0 - 2025-02-26 = * Add - Add E2E tests for currency switching at checkout. * Add - Add GrabPay payment method details to the View Transaction page. * Add - Add GrabPay to the settings page when eligible. * Add - Add loader indication for Stripe notifications banner component * Add - Add payment method logos to checkout block card label. * Add - Adds store_id property to body of WooPay tracker events. * Add - Add Stripe embedded account notifications component on the Overview page * Add - Add WeChat Pay settings. * Add - Add WeChat Pay support to checkout. * Add - feat: add initial support for Alipay settings * Add - feat: add order notes to tokenized ECE GooglePay/ApplePay on blocks checkout * Add - Implement checkout for GrabPay payments. * Add - Pass the business name to the express checkout handler. * Fix - Avoid duplicated payment methods detection notice on new stores. * Fix - Avoid PMME init for unavailable BNPL use-cases. * Fix - Ensure multi-currency widget markup getter don't throw errors. * Fix - Ensures that the tokenised cart for ECE implementation is disabled by default. * Fix - fix: attribute selection from PDPs with tokenized ECE * Fix - fix: tokenized ECE improvements with WC Deposits, Bookings, Bundles. * Fix - Fixed an issue with the copy test card number on Astra theme * Fix - Fixes incorrect defined statement for WC_Install::STORE_ID_OPTION constant. * Fix - Fixes thrown PHP warning in checkout config. * Fix - Fix Stripe KYC flow Woo color * Fix - Fix the default value of the FRT advanced options. * Fix - Improve how Stripe Billing integration is loaded to prevent unnecessary queries on every page load. * Fix - Prevent fatal errors when subscription is deactivated due outdated version. * Fix - Set orderby to `none` for order queries where checking if order exists. * Fix - Update the size of Woo logo on the test drive onboarding page. * Update - Add a notice indicating that overview page needs to be reloaded after completing the requirements in the embedded component. * Update - Add JSON-formatted minimum amount details to the amount too small error message. * Update - feat: GooglePay/ApplePay refactor to leverage Store API is enabled by default - second try. * Update - Improve CSV export experience and deliver consistent reports via async transact platform service-based CSV exports. * Update - Remove the receipt details tab in the card readers page. * Update - update: add payment method functionality to honor WC rate limit. * Update - update: tokenized ECE to exclude itemized items on rounding discrepancies of totals. * Update - Update copy in the Woopayments Reset account modal for incomplete onboarding accounts * Update - Update Credit Card / Debit Card label to Cards * Update - Update handling for 0 feeAmount to be consistent with details page. * Update - Update handling for refund processing in case of insufficient funds. * Update - Update handling of PR as a country in the terminal locations endpoint. * Update - Update the copy in the existing modal for resetting a WooPayments account. * Dev - Add critical tag to tests that are part of the critical flows. * Dev - Add new E2E tests to make sure a non-admin user, such as an editor, can access the wp-admin without errors. * Dev - Add Slack integration to Playwright E2E tests. * Dev - Cache calls to wc_get_orders in the incentives class * Dev - Extract the Account Session logic from Embedded components onboarding into a reusable utility hook. * Dev - Fix potential flakiness while creating a page with the checkout block. * Dev - Remove Puppeteer references. * Dev - Remove routing to the partially re-engineered payment process" * Dev - Update E2E NPM scripts. * Dev - Update E2E readme doc. * Dev - Update E2E setup scripts to avoid conflicts with other containers and permissions. * Dev - Update Github actions and workflows. Change e2e tests directory structure. = 8.9.2 - 2025-02-21 = * Fix - Fixed WooPay opt-in race condition = 8.9.1 - 2025-02-07 = * Fix - Ensures that the tokenised cart for ECE implementation is disabled by default. = 8.9.0 - 2025-02-04 = * Add - Add a popover to WooPayments to present all possible payment methods * Add - Added persistent column visibility preferences for reporting tables, allowing merchants to customize and save their preferred table view layouts across sessions. * Add - Add support for `WP_DEVELOPMENT_MODE`. Any non-empty setting will force the plugin into development mode. * Add - Add wcpay_capture_error_amount_too_small error type and minimum amount details when capturing payments below the supported threshold. * Add - Admin analytics page E2E tests for Playwright * Add - Enable "Reader fee" type filtering for transactions list. * Add - Show Bank reference key on top of the payout details page, whenever available. * Fix - Add currency code to fee breakdown when multi-currency is enabled, and currencies share the same symbol. * Fix - Added timestamp to the order note when terminal payment fails. * Fix - fix: add payment method page query initialization - ensuring that the WP_Query is initialized when checking for payment methods availability * Fix - fix: avoid ECE error when no address is provided on initialization * Fix - Fix cart subtotal amount when manually renewing a subscription. * Fix - Fixed fraud prevention token not available on blocks checkout * Fix - Fix flaky Multi-Currency test. * Fix - Fix global styling for popovers * Fix - Fix guest users being able to buy subscriptions with WooPay via Direct Checkout. * Fix - Fix incorrect payment method logos used on WooPayment Settings * Fix - Fix the amounts in the Filter by Price block to reflect the shopper's selected currency. * Fix - Pass theme editor footer and header styles to WooPay. * Fix - Prevent potential fatal when initializing the WooPay express checkout button. * Fix - Set a default minimum height to the WooPay button. * Fix - Update gateway form fields references to prevent errors. * Fix - Update set up live payments task list item copy * Fix - WooPay blocks checkout terms and condition default text * Update - Design improvements related to the overview page * Update - feat: GooglePay/ApplePay refactor to leverage Store API is enabled by default. Please contact us if you encounter new issues with these buttons. * Update - Improve loading experience in embedded KYC * Update - Remove date format notice across all admin pages. * Update - Removes Sofort payment method from settings and checkout, permanently deprecates Sofort from settings. * Update - Replace payments overview page connection success notice with a modal for live accounts. * Update - Simplify localization of CSV exports to use user language settings from WP Admin, allowing the CSV export to match the localization of the data presented in the admin UI. * Update - Update the loader image for test account creation * Update - Woo Colors and logos * Dev - Add E2E environment variables to Playwright config. * Dev - Add e2e tests for the multi-currency widget setup. * Dev - Converted E2E merchant-progressive-onboarding spec from Puppeteer to Playwright. * Dev - Convert merchant-orders-partial-refund spec from Puppeteer to Playwright. * Dev - Convert merchant admin transactions E2E test to Playwright * Dev - Convert order refund failure E2E tests to Playwright * Dev - Convert shopper-checkout-purchase spec from Puppeteer to Playwright * Dev - Convert shopper-multi-currency-widget spec from Puppeteer to Playwright * Dev - Convert shopper checkout save card and purchase test to Playwright * Dev - Convert shopper checkout with site editor theme spec to Playwright * Dev - Convert shopper checkout with UPE methods E2E test to Playwright, split saving card tests to separate file * Dev - Convert shopper free coupon checkout E2E test to Playwright * Dev - Convert shopper purchase multiple subscriptions E2E tests to Playwright * Dev - Convert Shopper WC Blocks saved card checkout and usage test to Playwright * Dev - Convert the merchant orders status change spec from Puppeteer to Playwright. * Dev - Convert the shopper-myaccount-payment-methods-add-fail spec from Puppeteer to Playwright. * Dev - E2E Playwright Migration: convert Admin Disputes spec * Dev - E2E Playwright Migration: convert Order Manual Capture spec * Dev - E2E Playwright Migration: convert shopper-checkout-failures spec to Playwright and remove Puppeteer spec. * Dev - E2E Playwright Migration: convert shopper-wc-blocks-checkout-purchase spec * Dev - Enhance log file format to provide more information about the request flow. * Dev - Fix flakiness in saved card tests caused by selling the same cart multiple times, triggering duplicate order protection * Dev - Migrate merchant subscription settings spec to Playwright * Dev - Migrate shopper subscription free-trial purchase E2E test to Playwright * Dev - Migrate shopper subscriptions - No signup fee E2E test to Playwright * Dev - Migrate the merchant subscriptions renew action scheduler spec from Puppeteer to Playwright. * Dev - Migrate the Shopper Renew Subscription spec to Playwright and remove the corresponding Puppeteer test. * Dev - Migrate the Shopper Subscriptions Manage Payments spec to Playwright and remove the corresponding Puppeteer test. * Dev - Optimise memory consumption when formatting log entries. * Dev - Port merchant subscription renewal E2E test to Playwright * Dev - Refactoring of snackbar checks in Playwright e2e tests * Dev - Refresh customer instance with REST API, replace customer creation by new order with anonymous customer * Dev - Removed unused get single transaction endpoint * Dev - Remove the subscriptions spec for Puppeteer * Dev - Restore activated currencies after all tests that deactivate current ones. = 8.8.0 - 2025-01-15 = * Add - Allow transactions filtered by Payment Method. * Add - Falback terms and conditions for WooPay. * Fix - Broaden billing field queries from form-scoped to document-scoped. * Fix - Checkout: Make sure the font size for PMME is smaller than the labels. * Fix - Ensure consistent formatting of refund notes with MC. * Fix - Fix incompatibility with some bookings plugins. * Fix - Fix JS exception when ECE payment fails. * Fix - Fix transaction list sorting by payout currency. * Fix - Improve the mobile rendering of the Balances box within Payment Overview. * Fix - Update currency conversion method for booking products. * Fix - Update WordPress.org readme.txt file. * Fix - Upgrade `@woocommerce/csv-export` package to v1.10.0 – fixes unnecessary escaping of negative values in CSV exports that was preventing numerical analysis in spreadsheet applications. * Fix - Zero dollar subscriptions with zero dollar renewals checkout error. * Update - Bump stable tag for 8.7.0. * Update - Improve BNPL PMME and icon placement in shortcode checkout. * Update - Remove the overlay that announces the rename from deposits to payouts. * Update - Replaced the term "VAT" with "Tax" in Documents Listing Page. * Update - round to nearest lowest denominator instead of ceiling before applying currency rounding settings. * Dev - Add the Playwright Pay for Order spec and remove the equivalent Puppeteer spec. * Dev - Add type assertion for disputes CSV export response to ensure type safety and fix TypeScript error. * Dev - Update phpunit-watcher dev package version. * Dev - Update the PHP version and the Xdebug version used in the E2E testing environment. * Dev - Update the Playwright Payment Gateways Confirmation spec and remove the Puppeteer Payment Gateways Confirmation spec. = 8.7.1 - 2025-01-14 = * Fix - Broaden billing field queries from form-scoped to document-scoped = 8.7.0 - 2024-12-25 = * Add - Add seller_message to failed order notes * Add - Add WooPay Klaviyo newsletter integration. * Add - Clickwrap terms and conditions support on WooPay * Add - Implement gateway method to retrieve recommended payment method. * Add - Migrate active capabilities from test-drive account when switching to live account. * Add - Refresh the cart and checkout pages when ECE is dismissed and the shipping options were modified in the payment sheet. * Fix - Add a rounding entry to Level 3 data for rare cases where rounding errors break calculations. * Fix - Added conditional use of Jetpack Config callback to avoid i18n notices. * Fix - Browser error no longer shows after dispute evidence submission * Fix - Ceil product prices after applying currency conversion, but before charm pricing and price rounding from settings is applied. * Fix - Consider WooPay eligibility when retrieving WooPay enable state in the settings. * Fix - Enable ECE for Virtual Variable Subscriptions with Free Trials. * Fix - Ensure captured transactions appear in the Transactions tab without requiring a page refresh. * Fix - Ensure ECE login confirmation dialog is shown on Blocks. * Fix - Ensure WooPay 'enabled by default' value is correctly set in sandbox mode. * Fix - Errors were incorrectly marked as info in logs. * Fix - fix: undefined $cart_contains_subscription * Fix - Fix blank Payments > Overview page when WC onboarding is disabled. * Fix - Fixed Affirm using black logo on dark themes * Fix - Fixed an issue where order metadata was not updated when capturing an order in the processing state. * Fix - Fixed UPE country detection in Checkout for non-logged in users * Fix - Fix filtering in async Disputes CSV export * Fix - Fix inconsistent alignment of the download button across transactions, payouts, and disputes reporting views for a more cohesive user interface. * Fix - Fix Jetpack onboarding URL query from "woocommerce-payments" to "woocommerce-core-profiler" * Fix - Fix payment method filtering when billing country changes in Blocks checkout. * Fix - Fix styling of transaction details page in mobile view. * Fix - Hide transaction fee on admin view order screen when transaction is not captured. * Fix - Load checkout scripts when they are not previously loaded on checkout page. * Fix - Localize postal code check label based on country. * Fix - Normalize HK addresses for ECE * Fix - Order notes for inquiries have clearer content. * Fix - Performance improvements for Disputes Needing Response task shown in WooCommerce admin. * Fix - Remove translations during initialization, preventing unnecessary warnings. * Fix - Restrict Stripe Link to credit card payment method and improve cleanup. * Fix - Set payment method title once title is known. * Fix - Show express checkout for products w/o shipping but where tax is included into price. * Fix - Support 'type_is_in' filter for Transactions list report, to allow easy filtering by multiple types. * Fix - Use "currency conversion fee" instead "foreign exchange fee" in payment timeline and various other places. * Fix - Use translatable strings on the fee breakdown tooltip of the payment settings screen. * Update - Add failure reason to failed payments in the timeline. * Update - Add support for showing `In-Person (POS)` as the transaction channel for mobile POS transactions in wp-admin Payments, enhancing visibility in both transaction lists and details. * Update - Adjust the go-live modal to match the latest design. * Update - Apply User-Defined Date Formatting Settings to WP Admin React Components * Update - Change 'Bank reference key' label to 'Bank reference ID' in Payouts list column for consistency. * Update - chore: renamed PRB references in GooglePay/ApplePay implementation docs and logs files to ECE. * Update - Ensure more robust selectors scoping to improve theme compatibility. * Update - Make test instructions copy icon use the same color as the text next to it * Update - Remove payout timing notice and update the help tooltil on Payments Overview page. * Update - Update confirmation modal after onbarding * Update - Update Embedded Components and MOX to support custom width and paddings. * Update - Update error messages for payment authorization actions to provide more specific and user-friendly feedback. * Update - Update Jetpack onboarding flow * Update - WooPay theming copy in the settings page * Dev - Add support for utilizing NOX capabilities as URL parameters during account creation. * Dev - Enable Payment Methods preselected by NOX after onboarding accounts * Dev - Fixing issue with parsing QIT authentication.Fixing issue with parsing QIT authentication. * Dev - Refine verification for disabling ECE on subscriptions that require shipping. * Dev - Remove hooks from customer and token services to dedicated methods * Dev - Update the tunelling setup. = 8.6.1 - 2024-12-17 = * Fix - Checkout: Fix error when wc_address_i18n_params does not have data for a given country * Fix - Skip mysqlcheck SSL Requirement during E2E environment setup = 8.6.0 - 2024-12-04 = * Add - Add Bank reference key column in Payout reports. This will help reconcile WooPayments Payouts with bank statements. * Add - Display credit card brand icons on order received page. * Fix - Add support to load stripe js asynchronously when it is not immediately available in the global scope. * Fix - Add the missing "Download" column heading label and toggle menu option to the Payments → Documents list view table. * Fix - Ensure ECE button load events are triggered for multiple buttons on the same page. * Fix - Ensure ECE is displayed correctly taking into account the tax settings. * Fix - Evidence submission is no longer available for Klarna inquiries as this is not supported by Stripe / Klarna. * Fix - fix: express checkout to use its own css files. * Fix - fix: missing ece is_product_page checks * Fix - Fix ECE Tracks events not triggering when WooPay is disabled. * Fix - Fix WooPay component spacing. * Fix - Fix WooPay trial subscriptions purchases. * Fix - Make sure the status of manual capture enablement is fetched from the right place. * Fix - Prevent express checkout from being used if cart total becomes zero after coupon usage. * Fix - Resolved issue with terminal payments in the payment intent failed webhook processing. * Fix - Set the support phone field as mandatory in the settings page. * Fix - Update Link logo alignment issue when WooPay is enabled and a specific version of Gutenberg is enabled. * Fix - Use paragraph selector instead of label for pmme appearance * Fix - Validate required billing fields using data from objects instead of checking the labels. * Update - Avoid getting the appearance for pay for order page with the wrong appearance key. * Update - chore: rename wrapper from payment-request to express-checkout * Update - feat: add `wcpay_checkout_use_plain_method_label` filter to allow themes or merchants to force the "plain" WooPayments label on shortcode checkout. * Update - refactor: express checkout initialization page location checks * Update - refactor: express checkout utility for button UI interactions * Dev - Allow redirect to the settings page from WCPay connect * Dev - chore: removed old PRB implementation for ApplePay/GooglePay in favor of the ECE implementation; cleaned up ECE feature flag; * Dev - Disable visual regression testing from Playwright until a more reliable approach is defined. * Dev - Ensure proper return types in the webhook processing service. * Dev - fix: E_DEPRECATED on BNPL empty PMME * Dev - Fix return types * Dev - Update snapshots for E2E Playwright screenshots = 8.5.1 - 2024-11-25 = * Fix - fix: remove "test mode" badge from shortcode checkout. = 8.5.0 - 2024-11-13 = * Add - Add country-specific test card numbers for credit card testing * Add - Add risk level information to the fraud and risk box on the order details page. * Add - Add support for allowedShippingCountries in Express Checkout Element. * Fix - Avoid PHP warnings for requests with an empty path. * Fix - BNPL methods now work properly in Pay for Order when they are available. Default values are also provided when available. * Fix - fix: payment method icon alt text * Fix - Fix compatibility issues with CustomSelectControl component styles for WordPress 6.7 * Fix - Fix duplicate saving of 3DS card entry after checkout * Fix - Fixed single product page view tracks when BNPL and PRB payment methods are inactive. * Fix - Fix PMME appearance in checkout * Fix - Fix settings display on the advanced fraud protection page. * Fix - Fix WooPay button preview in plugin settings. * Fix - Fix WooPay duplicated Save my info section. * Fix - Include missing scripts that handle refunds for non credit card payments in the order details page. * Fix - Introducing consistency in colors for deposits across pages * Fix - Prevent dead space on product pages when no BNPL offers are available. * Fix - Provide backwards-compatible method for retrieving the multi-currency instance. * Fix - Register Expresss Checkout block only when enabled in the settings * Fix - Remove unnecessary '.woocommerce-table' css overrides to fix WC Analytics styling and a11y issues * Fix - Use 'Withdrawal' and 'Deducted' labels when referring to withdrawal deposits, to more accurately communicate the type of transaction that has occurred * Update - Add in-memory cache fallback for our database-cached objects in case of database write failures. * Update - Decoupled Multi-currency module from gateway dependencies. * Update - Improvements to events during onboarding flow. * Update - Refactor loadStripe for Express Checkouts. * Update - Rename 'deposit' to 'payout' across various places in the WooPayments admin UI, docs and doc URLs. * Update - update: payment method "test mode" label at checkout to be displayed only when payment method is selected * Update - update: show LPM payment method icon on order success page * Update - Use preview versions of the actual ECE buttons in the Block editor. * Update - When tracking is enabled, send a tracks event when the merchant is redirected to a Stripe Capital offer. * Dev - Add Jest tests for the disputed order notices * Dev - Add unit tests for the Capital Loans page component. * Dev - Bump WC tested up to version to 9.4.0 = 8.4.0 - 2024-10-23 = * Add - Add test mode badge to classic checkout and add payment method. * Add - Using Floating Labels with Stripe Appearance API for Blocks Checkout * Fix - Converting text color rgba to hex to prevent Stripe warning * Fix - Fix the color contrast of links within tooltips to improve readability. * Fix - Omit the test mode badge in the change payment method form for subscriptions. * Fix - Pass container styling data to WooPay * Fix - Prevented detaching payment methods from live Stripe accounts when working in non-production environments. * Fix - Rendering Test Model badge only for Credit Card * Fix - Stop enqueuing woopay-express-button.css to prevent 404 errors * Fix - The amounts used by the PMMEs will match the displayed price of the product regardless of the tax settings. * Fix - WooPay user registration via classic checkout * Update - Add support for the style controls for the WooPay button * Update - chore: remove deprecated is_in_dev_mode() and is_in_test_mode() methods * Update - Payments task onboarding flows skip the Connect page. * Dev - Bump WC tested up to version to 9.3.3. * Dev - fix: prevent multiple instances of WC_Payments_Apple_Pay_Registration * Dev - Fixed wrong utils path that would prevent checkout with WooPay OTP * Dev - Migrate WizardTaskItem and CollapsibleBody components to TypeScript, making the className prop optional. * Dev - Use official `phpmyadmin` Docker Hub container image = 8.3.1 - 2024-10-16 = * Fix - Auto-enabled WooPay for new accounts. * Fix - Load Stripe with merchant account's key when checking payment method availability. = 8.3.0 - 2024-10-03 = * Add - Add compatibility with the buttonAttributes API from Woo Blocks * Add - Add rate limiter to compatibility data updates * Add - Add UTC to the date time column header of transactions list page. * Add - Disable save changes button until a setting has changed. * Add - Enables Klarna with amount limitations for FR country code. * Add - Provide locale to Express Checkout Element. * Add - Redesigned the Payment Method labels for the Blocks Checkout * Add - Show a notice in Payments > Settings > Deposits if there is an error with the bank account. * Add - Updates to the Embedded KYC to ensure compatibility with Progressive Onboarding * Fix - Allow the network saved card payment method setting to be overridden by the `wcpay_force_network_saved_cards` filter * Fix - Create div container element with JS dynamically. * Fix - Do not display BNPL methods and PMME when WooPayments is disabled * Fix - Fixed CC form input fields appearance when using RGBA * Fix - Fixed invalid appearance warnings * Fix - Fix event prefix duplication in Tracks events * Fix - Handle loadError in ECE for Block Context Initialization. * Fix - Prevent failures and notices related to trying to schedule AS jobs before init. * Fix - Prevent multi-currency conversion during a REST API request. * Fix - Remove text color from WooPay component. * Fix - Rendering Test Model badge only for Credit Card * Fix - Sync phone changes with WooPay phone field. * Fix - Update payment methods when manual capture is enabled. * Update - Default express checkout button label to "Only icon". * Update - Remove feature flag to make embedded KYC enabled by default * Update - Some minor styling updates on the Onboarding form. * Update - Update express payment methods with a title, description and gatewayId * Dev - fix: prevent multiple instances of WC_Payments_Apple_Pay_Registration * Dev - Fix gutenberg blueberry focus color * Dev - Fix progressive onboarding e2e test * Dev - Some refactors to embedded KYC logic. = 8.2.2 - 2024-09-24 = * Fix - Fix WooPay pre-checking place order bug when buying a subscription. = 8.2.1 - 2024-09-13 = * Fix - Create div container element with JS dynamically. = 8.2.0 - 2024-09-11 = * Add - add: test instructions icon animation * Add - Added Embdedded KYC, currently behind feature flag. * Fix - Avoid unnecessary account data cache refresh on WooPayments pages refresh. * Fix - Check payment method is available before rendering it. * Fix - Disables custom checkout field detection due to compatibility issues and false positives. * Fix - Disables testing instructions clipboard button on HTTP sites when navigator.clipboard is undefined. * Fix - fix: missing translations on testing instructions. * Fix - fix: platform_global_theme_support_enabled undefined index * Fix - fix: testing instructions dark theme support * Fix - Fix caching with tracking cookie. * Fix - Fixed an issue where the Connect page would scroll to the top upon clicking the Enable Sandbox Mode button. * Fix - Fixed default borderRadius value for the express checkout buttons * Fix - Fix shipping rates retrieval method for shortcode cart/checkout. * Fix - Fix support for merchant site styling when initializing WooPay via classic checkout * Fix - Fix WooPay direct checkout. * Fix - Handle loadError in ECE for Block Context Initialization. * Fix - Move woopay theme support checkbox to the appearance section. * Fix - Pass appearance data when initiating WooPay via the email input flow * Fix - Prevent preload of BNPL messaging if minimum order amount isn't hit. * Fix - Redirect user to WooPay OTP when the email is saved. * Fix - Remove obsolete ApplePay warning on wp-admin for test sites. * Fix - Update cache after persisting the User session via WooPay * Fix - Updates test mode instructions copy for cards at checkout. * Update - update: payment method fees in one line * Update - Update Jetpack packages to the latest versions * Dev - Fix failing e2e tests for saved cards. * Dev - Fix Klarna product page message E2E test after the contents inside the iframe were updated. * Dev - Migrate Klarna E2E tests to playwright. Reduce noise in E2E tests console output. * Dev - Migrate multi-currency e2e tests to Playwright. = 8.1.1 - 2024-08-29 = * Fix - Ensure 55px is the maximum height for Apple Pay button. * Fix - Fixed sandbox mode accounts being able to disable test mode for the payment gateway settings. = 8.1.0 - 2024-08-21 = * Add - Add button rules to appearance * Add - Add heading rules to appearance * Add - Add link rules to appearance * Add - Adds skeleton loading element for BNPL payment messaging element on product details page. * Add - Add tokenized cart PRB support to cart and checkout blocks. * Add - Add WooPay global theme support flag * Add - Enhance Sandbox mode onboarding by skipping KYC and making it fully automated for all supported countries. * Fix - Added better error message when mandate is invalid. * Fix - Do not enqueue Cart scripts if WooPayments is not enabled * Fix - fix: alignment of info icon w/ WooPay/Link settings * Fix - fix: cart association on tokenized PRB orders via custom session handler * Fix - fix: platform_global_theme_support_enabled undefined index * Fix - fix: translatable strings around currencies list * Fix - Fix GooglePay button missing in Safari. * Fix - Fix onboarding redirect loop when starting from Woo Settings Payments. * Fix - Fix spacing on Express Checkout buttons. * Fix - Fix uncaught error on the block based Cart page when WooPayments is disabled. * Fix - Fix WooPay checkboxes while signed in. * Fix - If a payment method fails to be created in the frontend during checkout, forward the errors to the server so it can be recorded in an order. * Fix - Reverts changes related to Direct Checkout that broke the PayPal extension. * Fix - Translate hardcoded strings on the Connect page * Update - refactor: separate BNPL methods from settings list * Update - Updated the integration between WooPayments Multi-Currency and Product Add-Ons. * Update - Update outdated express checkout settings notification. * Dev - Add e2e tests for critical flow: merchant account balance overview * Dev - Bump WC tested up to version to 9.2.0 * Dev - Fix Klarna and Refund Failures E2E tests * Dev - Match the Node version in nvm with the minimum version in package.json. * Dev - Remove unnecessary console.warn statements added in #9121. * Dev - Update bundle size checker workflow to support node v20 * Dev - Migrate to Docker Compose V2 for test runner environment setup scripts = 8.0.2 - 2024-08-07 = * Fix - Add opt-in checks to prevent blocking customers using other payment methods. * Fix - Fix error in Express Checkout Element use with coupons. * Fix - Only enable Direct Checkout when WooPayments gateway is enabled. = 8.0.1 - 2024-07-31 = * Fix - Reverts changes related to Direct Checkout that broke the PayPal extension. = 8.0.0 - 2024-07-31 = * Add - Add ECE support for multiple product types. * Add - Add ECE support for WooCommerce Deposits. * Add - Added filter to enable updating Level 3 data based on order data. * Add - Add independent ECE instances in WC Blocks. * Add - Add the new payment method logos to the connect page. * Add - Apply WooPay direct checkout flow to alternative mini cart checkout button. * Add - Pass Blocks checkout appearance on init WooPay * Add - Pass product, blocks cart, classic cart and checkout on get WooPay session * Add - Set ECE as Default for Express Checkout Buttons unless it was disabled. * Add - Support adding tax details (corporate number) for Japan merchants (so can generate tax documents for consumption tax, aka VAT). * Add - Use new payment_method_domains endpoint for domain registration. * Add - Use Stripe's Express Checkout Element (ECE) for express checkout button previews in the settings when ECE is enabled. * Fix - Allow Afterpay gateway to process payments when the state/county is optional for GB and NZ addresses. * Fix - Allow the purchase of physical subscriptions using ECE if no shipping options are defined. * Fix - Disable ECE for non shipping products if Tax is calculated on billing address. * Fix - Disable WooPay’s Direct Checkout feature if WooPayments is not enabled as payment gateway. * Fix - Ensure that 'wcSettings' exists before attempting to use 'wcSettings.wcBlocksConfig'. * Fix - Ensure the 'Proceed to Checkout' button does not collapse when adding a loading spinner, in the Direct Checkout flow. * Fix - Fix Express Checkout Element button width. * Fix - Fixing fatal errors when subscription classes are not available * Fix - Fix minimum width for express checkout buttons on checkout block. * Fix - Fix payment method title for Express Checkout Element orders. * Fix - Fix UI state when processing ECE payment on Cart Block. * Fix - Fix WooPay opt-in blocks field on WooCommerce 9.1+. * Fix - Make Google Pay ECE compatible with WooPay. * Fix - Prevent WooPay opt-in from blocking the place order button * Fix - Remove bullet from WooPay button on cart block in Safari * Fix - Replace WooPay's development environment constants with global variables. * Fix - Send optional fields data to WooPay. * Fix - Updates payments settings copy to support pay now with klarna. * Fix - Use the customer id saved in the subscription to process renewal payments. * Update - Deprecate Giropay. * Update - Increase font size and update the design of the WooPay button * Update - Reorder onboarding wizard business types to always have Company as the first option. * Update - Set express checkout max button height to 55px * Dev - Add error logging to ECE critical endpoints. * Dev - Avoid using deprecated hook for processed checkout order. * Dev - Bump WC tested up to version to 9.1.2 * Dev - Update bundle size checker workflow to support node v20 * Dev - Update node to v20 = 7.9.2 - 2024-07-18 = * Fix - Fix store connection loop for onboarding flows started from the Woo > Settings > Payments page. = 7.9.1 - 2024-07-11 = * Fix - Fix Documents API regex to allow documents with dashes in name to be viewed. = 7.9.0 - 2024-07-10 = * Add - Add a separate transient to save UPE appearance styles for the Add Payment Method standalone page. Correct regression that prevented proper styles calculation in the shortcode checkout. * Add - Add Pay for Order support in Express Checkout Elements. * Add - Add support for configuring button radius when ECE is enabled * Add - Add support for ECE elements on the Shortcode Cart and Checkout pages * Add - Add support for the Express Checkout Element on product pages. * Add - Add telemetry events from PRBs into ECE. * Add - Ensure shoppers can still checkout, even when WooPay is slow or unavailable. * Add - feat: tokenized cart PRBs on shortcode cart and checkout behind feature flag. * Add - Support style settings for ECE buttons * Fix - Clearly display available instant deposit amount on notice and button label on Payment Overview page * Fix - Disable Stripe Link in ECE. * Fix - Disable WooPay for suspended and rejected accounts. * Fix - Display an invalid address error instead of generic one in the checkout form when Afterpay is selected as payment method * Fix - Display payment error message in the Payment context with Blocks. * Fix - fix: display refund amount w/ tokenized cart PRBs * Fix - fix: pricing decimal formatting for tokenized cart * Fix - fix: tokenized PRBs payment type * Fix - Fixed an error when renewing subscriptions without a billing country * Fix - Fix output for compatibility data. * Fix - Fix transaction list and document list advanced filter styling issue preventing dates to be input on mobile screens. * Fix - Fix WooPay Direct Checkout feature check. * Fix - Fix WooPay OTP modal not rendering on the shortcode checkout if BNPL methods are available. * Fix - Hide payment methods with domestic transactions restrictions (Klarna, Affirm, Afterpay) when conditions are not met. * Fix - Make the search box, and typed search term visible clearly on the 'Payments > Transactions' page, when there are too many existing search tags. * Fix - Properly wait for tokenized cart data updates before refreshing PRB data. * Fix - Retrieve saved tokens only relevant for the specific payment gateway. * Update - Deprecate Giropay. * Update - Update payment receipt settings to remove mention of the printed receipts. * Dev - Add validation for path variables. * Dev - Migrate Affirm and Afterpay payment method components to TypeScript. * Dev - Prevent infinite loop in usePaymentFailHandler effect * Dev - Refactor redirects logic in payments = 7.8.1 - 2024-06-25 = * Fix - Fix "Dispute not loaded" error that was affecting responding to disputes. = 7.8.0 - 2024-06-19 = * Add - Add a feedback survey modal upon deactivation. * Add - Add new select component to be used for reporting filters, e.g. Payments overview currency select * Add - Add payment processing using ECE in the Blocks checkout and cart pages. * Add - Add the WooPay Direct Checkout flow to the classic mini cart widget. * Add - Add woocommerce-return-previous-exceptions filter * Add - Enable adapted extensions compatibility with Direct Checkout. * Add - feat: add pay-for-order support w/ tokenized cart PRBs * Add - Fix ECE not working without WooPay. * Add - Reset notifications about duplicate enabled payment methods when new plugins are enabling them. * Fix - Fall back to credit card as default payment method when a payment method is toggled off. * Fix - fix: address normalization on checkout for tokenized cart PRBs * Fix - fix: itemized totals & pending amount on tokenized cart * Fix - fix: Store API tokenized cart payment method title * Fix - Fixes some cases where redirects to the onboarding will open in a new tab. * Fix - Fix input-specific credit card errors. * Fix - Fix Payment method title for PRBs not displaying correctly because of ECE code. * Fix - Fix Teams for WooCommerce Memberships on product WooPay Express Checkout Button. * Fix - Fix WooPay Direct Checkout feature check. * Fix - Improve consistency of Manage button for different WooPayments KYC states * Fix - Make it so that the WooPay button is not triggered on Checkout pages when the "Enter" key is pressed on a keyboard. * Fix - Prevent account creation during WooPay preflight request. * Update - chore: update incompatibility notice wrapping * Update - Declare compatibility with the Cart and Checkout blocks. * Update - Improve the transition from the WCPay KYC to the WC Admin Payments Task * Update - Update the Payments Overview screen with a new currency selection UI for stores with multiple deposit currencies * Update - Use FILTER_SANITIZE_EMAIL to sanitize email input * Dev - Add New_Process_Payment_Exception * Dev - Add Order_ID_Mismatch_Exception * Dev - Add sh support in pre-push husky script. * Dev - Add validation for path variables. * Dev - Bump WooCommerce Tested To version to 8.9.2 * Dev - Bump WooCommerce Tested To version to 8.9.3 * Dev - chore: EPMs to always send shipping phone * Dev - Clean up and refactor some old code which is no longer in use. * Dev - Fix PHPStan warnings. * Dev - Fix unused parameter phpcs sniffs in checkout classes. * Dev - Improve test coverage of upe.js and rename isPaymentMethodRestrictedToLocation to hasPaymentMethodCountryRestrictions * Dev - Remove redundant wrapper around method invocation. = 7.7.0 - 2024-05-29 = * Add - Add share key query param when sending data to Stripe KYC. * Add - Add the WooPay Direct Checkout flow to the blocks mini cart widget. * Add - feat: add multi-currency support to Store API. * Add - feat: error message on 1M+ amount. * Add - feat: tokenized cart PRBs on PDPs via feature flag. * Add - Render ECE buttons behind a feature flag. * Fix - Charm pricing and rounding options corrected for all currencies that aren't presented with decimal points. * Fix - Fix "Pay for order" infinite loading when submitting form without payment details. * Fix - fix: remove WooPay checkout pages scripts from non-checkout pages. * Fix - fix: settings notices consistency. * Fix - fix: Store API tokenized cart nonce verification. * Fix - Fix a bug in Tracks where shopper events are not fired properly. * Fix - Fix ECE error in the blocks checkout when PRBs are disabled. * Fix - Fix Payment block render error while editing the block checkout page. * Fix - Fix shortcode orders when using WooPay Direct Checkout. * Fix - Improve visibility of WooPay button on light and outline button themes. * Fix - Updating saved payment method billing address before processing the payment. * Update - Do not auto-redirect to WooPay on page load. * Update - Pass previous exception with exception. * Update - Removed deprecated deposit_status key from account status. * Update - Remove public key encryption setting from WooPayments settings. * Update - Update XPF currency formatting. * Dev - Add command to run QIT PHPStan tests. * Dev - Add local release package support for PHPStan. * Dev - Bump tested up to version for WP to 6.5 and WC to 8.9.1. * Dev - Fix Klarna E2E tests. * Dev - Guarantee REST intialization on REST request context (avoiding rest_preload_api_request context). * Dev - Upgrade jetpack sync package version. = 7.6.0 - 2024-05-08 = * Add - Add additional data to Compatibility service * Add - Add User Satisfaction Survey for Payments Overview Widget * Add - Detect payment methods enabled by multiple payment gateways. * Add - Display BNPL payment method logos on the thank you page. * Add - Non user-facing changes. Behind feature flag. Add tooltip messages to tiles within Payment activity widget * Add - Not user-facing: hidden behind feature flag. Use Reporting API to fetch and populate data in the Payment Activity widget. * Add - Pre check save my info for eligible contries * Add - Support for starting auto-renewing subscriptions for In-Person Payments. * Fix - Add notice when no rules are enabled in advanced fraud settings * Fix - Adjust positioning of BNPL messaging on the classic cart page. * Fix - Avoid updating billing details for legacy card objects. * Fix - Ensure the WooPay preview in the admin dashboard matches the actual implementation. * Fix - fix: BNPL announcement link. * Fix - fix: Stripe terms warning at checkout when Link is enabled * Fix - Fix issue with transient check related to fraud protection settings. * Fix - Fix order notes entry and risk meta box content when a payment is blocked due to AVS checks while the corresponding advanced fraud rule is enabled. * Fix - Fix type error for fraud outcome API. * Fix - Fix WooPay tracks user ID for logged in users. * Fix - Hide Fraud info banner until first transaction happens * Fix - Improve merchant session request with preloaded data. * Fix - Improve signing of minimum WooPay session data. * Fix - Make sure an explicit currency code is present in the cart and checkout blocks when multi-currency is enabled * Fix - Prevent Stripe Link from triggering the checkout fields warning * Fix - Remove risk review request from the transactions page. * Fix - Resolves "Invalid recurring shipping method" errors when purchasing multiple subscriptions with Apple Pay and Google Pay. * Fix - Revert: Add Multi-Currency Support to Page Caching via Cookies. * Update - Add source param to onboarding and complete KYC links * Update - Add support of a third-party plugin with PRBs into duplicates detection mechanism. * Update - Remove feature flag for the pay-for-order flow * Dev - Add Playwright e2e test suite ready for incremental migration and visual regression testing * Dev - Avoid warnings about fatal error during plugin update due to problems with plugin initialization. * Dev - Remove legacy method from `WooPay_Utilities`. * Dev - Remove obsolete docker-compose key `version` * Dev - Upgraded jetpack sync package version. = 7.5.3 - 2024-04-22 = * Fix - Fix subscription renewals exceptions * Dev - Remove deprecated param from asset data registry interface. = 7.5.2 - 2024-04-22 = * Fix - Bugfix for failing subscription renewal payments. * Dev - Remove deprecated param from asset data registry interface. = 7.5.1 - 2024-04-18 = * Fix - Avoid updating billing details for legacy card objects. * Fix - fix: BNPL announcement link. = 7.5.0 - 2024-04-17 = * Add - Add a parent wrapper component for Payment Activity widget. This will be visible on the Payments Overview page * Add - Add a task on WooCommerce Home page to remind accounts operating in sandbox mode to set up live payments. * Add - Add BNPL messaging to cart page. * Add - Add BNPL terms to checkout payment methods. * Add - Added support to Cartes Bancaires * Add - Adding a tracking event for external redirects to finish setup and start receiving deposits. * Add - Add Multi-Currency Support to Page Caching via Cookies. * Add - Extend 'skip WooPay' flag to user session. * Add - feat: BNPL April announcement. * Add - Improve payment settings UX. * Add - Not user-facing: hidden behind feature flag. Add an empty state view for the Payments Activity widget. This is shown when the merchant is yet to have any transactions over WooPayments. * Add - Not user facing - Changes are behind a feature flag. Adds the basic UI scaffold for the Payments Activity widget. * Add - Prepopulate Vertical selection in the onboarding form based on Woo Core selection. * Fix - Add an instructive error message when customer tries to use 2 different currencies for Stripe Billing subscriptions. * Fix - Address PHPCS reports in checkout classes. * Fix - Adds a check to see if the session exists before calling get() * Fix - Change IP country rule after country settings are changed in WC settings page * Fix - Defensive check for cart block PMME which hasn't yet been deployed. * Fix - Don't register WooPay Order Webhook if account is rejected. * Fix - Don't register WooPay Order Webhook if account is under review. * Fix - Ensure "Proceed to checkout" button's loading spinner doesn't affect button spacing when Direct Checkout is enabled. * Fix - Ensure that the currency configurations are set correctly when multi-currency is enabled. * Fix - Ensure we avoid an infinite recursive call stack through 'wc_get_price_decimal_separator' filter. * Fix - fix: error message on 402 status code * Fix - Fix an incorrect warning about Puerto Rico being unsupported by WooPayments * Fix - Fix collision between WooPayments header and Woo Express survey banner. * Fix - Fix Decline button state for Accept loading on ToS modal * Fix - Fixed Afterpay logo size on settings page * Fix - Fixed billing address error for subscription without some billing details * Fix - Fixed optional billing field validation * Fix - Fix Fatal Error showing when connect to Jetpack on localhost * Fix - Fix JS error when clicking GPay button on blocks checkout with subscription (w/sign up fee) in cart. * Fix - Fix payment icons on connect page (Klarna, Afterpay) * Fix - Hide account tools finish setup button for accounts completed the KYC and change it's link to Stripe KYC instead of the Stripe Express. * Fix - Remove extra WooPay icon on connect page * Fix - Remove incorrect "UTC" label from the time column of Transactions page * Fix - Remove redundant message after the account is onboarded * Fix - Resolves "Invalid recurring shipping method" errors when purchasing multiple subscriptions with Apple Pay and Google Pay. * Fix - Sync discount brakedown with how server processes it * Update - Adding a tracking event for external redirects to update account details, more consistent behaviour for redirects. * Update - Cleanup unused payment confirmation code. * Update - Reduce the visual footprint of the sandbox mode notice. * Update - Remove @wordpress/data dependency in the email input iframe file * Update - Remove ToS acceptance copy from all entrypoints in MOX * Update - Remove unwanted css overrides on the Payment activity widget wrapper * Update - Replace deprecated filter. * Update - Update Discover and Diners logos * Update - Updated Link by Stripe logo * Update - Update links across the plugin from woo.com to woocommerce.com (previous platform HQ URL). * Update - Update MOX cancellation to consistently redirect back to the payment connect page. * Dev - Address update PHPCS error - a single one recommending escaping an exception message * Dev - Add TypeScript development guidelines * Dev - Add VariableAnalysis sniffs for better ergonomics around unused and undefined variables * Dev - Changes are not user facing, behind a feature flag. Refactor - tooltip component flattened in place, instead of calling it from a separate file. * Dev - Clear WP cache on writes inside Database_Cache * Dev - Escaping error logs and ignoring noticese where there are no issues. * Dev - Fix e2e tests for BNPL checkout * Dev - Fixed phpcs errors * Dev - Fixes to comply with updates to PHPCS linter. * Dev - Ignore alternative function WordPress PHPCS sniffs in the GH workflows and tests * Dev - Increase unit test coverage for WooPay direct checkout flow. * Dev - Payments Activity - add scaffolding for Total Payment Volume. * Dev - refactor: reduce wp-data dependency on shortcode checkout * Dev - Updated PHPCS and sniffs used for static analysis * Dev - Updates to remove deprecated function usage in the Jetpack Connection manager. Doesn't affect functionality. = 7.4.0 - 2024-03-27 = * Add - Add account reset for sandboxes * Add - Add a loading spinner to the "Proceed to Checkout" button in shortcode cart. * Add - Add data telemetry for the Proceed to Checkout button and the WooPay direct checkout. * Add - Added a notice about custom checkout fields conflicting with express checkouut * Add - Added a notice to inform the merchant when the payout bank account is in errored state * Add - Added a notice to the Deposits Listing screen when deposits are paused * Add - Add WooPay direct checkout flow behind a server-side feature flag. * Add - Add woopayMinimumSessionData in wcpayConfig when Express Checkout button's disabled o on car page. * Add - Customizing BNPL messaging with Appearance API * Add - Handle refunds triggered externally, outside WP Admin * Add - Preload WooPay session data for WooPay Direct Checkout flow. * Fix - Add empty validation when quering order by meta key and meta value * Fix - Add settings object for every gateway * Fix - Allow WooPay button preview on settings page * Fix - Apply the WooPay Direct Checkout flow only to the "Proceed to Checkout" button, in cthe classic cart. * Fix - Ensure card gateway is not running through the settings migration. * Fix - Ensure every gateway has individual settings object. * Fix - Ensure WooPay Direct Checkout continues to work as intended when WooPay Express Checkout is disabled on the cart page. * Fix - fix: "Add payment method" page initialization on missing UPE styles * Fix - fix: better error message when saved card and amount too large * Fix - Fixed a 1px gap on the right side of some payment method icons in transaction details. * Fix - Fixed Clearpay aria-label for UK sites * Fix - Fixes some instances where Stripe UPE styles add a black background to input fields. * Fix - Fix the cursor pointer when hovering over disabled checkboxes in Advanced Settings * Fix - Fix WooPay direct checkout eligibility checks. * Fix - Hide the option to refund in full from the transaction details menu when a transaction is partially refunded. * Fix - Inform hard blocked merchants they're under review * Fix - Load deposit overview only when deposit is present * Fix - Move test mode transactions notice to the top of the page. * Fix - Order completed and refunded emails are no longer sent when a dispute is closed. * Fix - Preventing stock quantity from being reduced twice. * Fix - Re-enable Direct-to-Checkout Feature Flag in WooPay OTP Iframe. * Fix - strtolower deprecation warning no longer appears on the Order Received and View Order pages when using PHP version 8.1+ * Fix - Used client side navigation to improve the UX for "View Deposit History" * Fix - Uses WCPayAPI to initialise Stripe for BNPL payment element, includes necessary required parameters. * Update - Allow WooPay to request full session data from store. * Update - Clean up remaining unused code from a past experiment - `wcpay_empty_state_preview_mode`, done on Deposits list. * Update - Make the order note for `dispute funds withdrawn` event clearly mention that the dispute amount and fee would be deducted from the next deposit. * Update - Remove mention of test mode from general settings help text. * Update - Updated deposits API documentation to add default_external_accounts element * Update - Updates to reduce the amount of steps required during onboarding flow. * Update - Update the Payments Connect page design and logic * Update - Validate deposit id before sending a request to fetch deposit. * Dev - Bump WC tested up to version to 8.7.0 and set Requires Plugins header. * Dev - Ensure pre-push hook understands terminal & non-terminal environments * Dev - Fix a bug in Tracks where admin events were not recorded properly * Dev - Implement a feature flag for the Payment Overview widget. * Dev - Minor refactor to disputes utility function inInquiry to make it accept only dispute status as a param, instead of whole dispute object. * Dev - refactor: validator package bundle size improvements = 7.3.0 - 2024-03-06 = * Add - Added confirmation modals for order status changes with pending authorizations * Add - Add migration script to cover situations with Link and WooPay both enabled after plugin update. * Add - Add support for bookable products to payment request buttons on product pages. * Add - Alert user when they try to leave advanced fraud settings page with unsaved changes * Add - E2E tests for card testing prevention measures * Add - feat: add UPE appearance filter * Add - Handle timeouts in direct to WooPay checkout flow. * Fix - Add checks for cart and checkout blocks to enqueue_style call. * Fix - Added a small separator line between converted currency and the next line item. * Fix - Added support for WooCommerce Deposits when using Apple Pay and Google Pay * Fix - Adjustments to the wording of confirmation modals on the order and transaction details pages. * Fix - Do not open the email input iframe when there is no wcpay as payment method * Fix - Do not open the email input iframe without wcpay payment method * Fix - Ensure gateways accessibility for use cases which don't require the gateway to be enabled * Fix - Fees are now correctly applied to the Capture amount instead of the Authorize amount. * Fix - fix: add confirmation modal when disabling WooPayments from settings page * Fix - fix: ensure BNPL enablement is not adding unnecessary currencies * Fix - fix: list of payment methods in disable confirmation modal * Fix - fix: multi-currency confirmation modal ui * Fix - fix: onboarding currency messaging for BNPLs * Fix - fix: payment method checkbox loadable state * Fix - fix: remove Afterpay EUR currency * Fix - Fix a few untranslated phrases in the plugin * Fix - Fix currency search input field size and margins in Woo Express. * Fix - Fixed billing address line 2 not being updated for saved payment methods * Fix - Fixed positioning on buy-now-pay-later Stripe element on product page. * Fix - Fixes Stripe appearances API implementation to support dark themes and new elements, includes amends to checkout logos. * Fix - Fix fraud prevention token not showing up on site editor checkout page * Fix - Fix typo of transaction type "Loan dispersement" to "Loan disbursement" on transactions list page. * Fix - Make tk_ai cookie usage cache compatible * Fix - Only load `blocks-checkout.css` on single product, cart, and checkout pages. * Fix - Rename log file to woopayments * Fix - Show an informative tooltip instead of estimating the next deposit date (which can be inaccurate). * Fix - Updating saved payment method billing address before processing the payment * Update - Add deposit schedule help tooltip to deposits list screen so it's consistent with Payments Overview. * Update - chore: convert payment request CTA to select. * Update - chore: remove UPE feedback survey * Update - On Payments Overview page, show total balance (pending + available) instead of pending balance. * Update - Refactor WooPay First Party Auth and WooPay Direct Checkout to reuse similar functionality. * Update - Update status chip to WooCommerce pill for transaction status component. * Dev - Add Klarna e2e tests. * Dev - Bump WC tested up to version to 8.6.0. * Dev - chore: remove BNPL feature flag check * Dev - chore: remove unused brandTitles property from PMs * Dev - Dev: additional check when pushing to protected branches. * Dev - Fire `wcpay_disputes_row_action_click` for any click to dispute details (not just `Respond` button). * Dev - Remove unused `/deposits/overview` data-fetching code. * Dev - Removing unsupported `deposits/overview` REST API. `deposits/overview-all` should be used instead. * Dev - Reverts removed REST controller class to prevent error on update from older versions of the plugin. = 7.2.0 - 2024-02-14 = * Add - Add compatibility data to onboarding init payload. * Add - Add WooPay direct checkout flow behind a feature flag. * Add - Apply localization to CSV exports for transactions, deposits, and disputes sent via email. * Add - Displaying Clearpay instead of Afterpay for UK based stores * Add - Enhance WooPay session validation * Add - Filtering APMs by billing country * Add - Show a notice to the merchant when the available balance is below the minimum deposit amount. * Add - Show charge id on payments details page, so merchants can grab it to fill out the dispute evidence form when needed. * Add - Showing "started" event in transaction timeline * Add - Support Stripe Link payments with 3DS cards. * Fix - Adjust WordPress locale code to match the languages supported by the server. * Fix - Displaying the correct method name in Order Edit page for HPOS * Fix - Don't instantiate `Fraud_Prevention_Service` in checkout if processing an authorized WooPay request. * Fix - fix: help text alignment with Gutenberg plugin enabled * Fix - fix: pay-for-order compatibility with other gateways * Fix - Fixed a bug where the 'deposits paused while balance is negative' notice was erroneously shown after an instant deposit. * Fix - Fixes Pay for Order checkout using non-card payment methods. * Fix - Fix losing cart contents during the login at checkout. * Fix - Merge duplicated Payment Request and WooPay button functionality . * Fix - Prevent coupon usage increase in a WooPay preflight check. * Fix - Prevent WooPay webhook creation when account is suspended * Update - Add source to the onboarding flow page and track it * Update - Refactor the WooPay checkout flow UX * Update - Some minor update to tracking parameters to pass additional data like Woo store ID. * Update - Stop relying on Woo core for loading plugin translations. * Dev - Added ENUM class for currency codes * Dev - Bump WC tested up to version to 8.5.2. * Dev - chore: removed deprecated functions since 5.0.0 * Dev - chore: remove unused checkout API methods * Dev - chore: remove unused gateway class methods * Dev - chore: remove unused isOrderPage return value from confirmIntent * Dev - chore: update colors on documentation pages * Dev - Comment: Bump qit-cli dependency to version 0.4.0. * Dev - E2E test - Merchant facing multi-currency on-boarding screen. * Dev - Fix for E2E shopper tests around 3DS and UPE settings * Dev - Refactoring the tracking logic * Dev - Refactor to how tracking events are defined for better readability. * Dev - Remove unnecessary tracks events for dispute accept success/error. * Dev - Update REST API documentation for deposits endpoints with changes to estimated and instant deposits * Dev - Update Tracks conditions = 7.1.0 - 2024-01-25 = * Add - Add active plugins array to compatibility data. * Add - Add post_types and their counts as an array to compatibility data. * Add - Add the active theme name of the blog to the compatibility service * Add - Expose the refund transaction ID in WooCommerce Order Refund API * Add - Select the proper payment element when using saved Stripe Link tokens or choosing to use Stripe Link for new email. * Add - Track filtering interactions on the Transactions page. * Fix - Allow subscription purchase via Payment Request when no shipping methods are present. * Fix - Allow zero-amount refunds for backwards compatibility with basic payment gateway and to allow re-stock of refunded orders. * Fix - Checking if wcpayPaymentRequestPayForOrderParams before using it in Pay for Order page * Fix - Checkout error when page URL is too long * Fix - Comment: Fix QIT security tests errors. * Fix - Fix incorrect test mode notice when left KYC early after going live from builder mode * Fix - Fix network error that occurs when viewing an test mode order with test mode disabled, and vice versa. * Fix - fix pay-for-order quirks and 3DS behavior * Fix - Fix Safe Mode message reversed host * Fix - Fix Stripe Link autofill on checkout. * Fix - Fix Stripe Link button alignment in the Checkout Block * Fix - Hide the transaction details refund menu for ineligble disputed transactions * Fix - Improve clarity & readability of disputed order notice (not all text bold). * Fix - Prevent possible fatal when using get_edit_post_link filter. * Fix - Re-render WooPay button when cart updates, when checkout updates. * Fix - Reinstate first deposit waiting period notice in payments overview (fix bug) * Fix - Remove unnecessary import statement which leads to a warning when first loaded * Fix - Resolved an error that would occur with WC 8.5.0 when editing a subscription customer from the admin dashboard. * Fix - Resolved an issue that caused ordering the Admin Subscriptions List Table to not work when HPOS is enabled. * Fix - Restock order items when performing full refund from transaction details page * Fix - Reverting to manual styling over native WordPress components to fix CSS defects on Analytics page * Fix - Send metadata in error message * Fix - Show the correct number of days in the new account waiting period notice. * Fix - Update WooPay tablet breakpoint. * Fix - Verify that order exists before offering "Partial refund" option on transaction details page. * Update - Changed the edit subscription product "Expire after" (Subscription length) so it more clearly describes when a subscription will automatically stop renewing. * Update - Pass currency parameter and not transaction_ids parameter when creating instant deposit. * Update - Store balance transaction ID in order metadata. * Update - Updated BNPL sorting in settings for consistency with onboarding. * Update - Update references to dev mode to use sandbox mode terminology. * Update - Updates to the styling of the onboarding mode selection page. * Update - Update style of notices within the deposits section of the settings screen. * Dev - Added enum class for country codes * Dev - Add new Tracks events to WooPay Save My Info checkbox * Dev - Allow test pipelines to pass by slightly adjusting HTML selectors * Dev - Merge UPE tests into the single and main gateway test file for unit and E2E tests. * Dev - Place order button Tracks * Dev - Track payment-request-button loads * Dev - Update jetpack dependencies for syncing. * Dev - Updates to account status logic to streamline it. * Dev - Update subscriptions-core to 6.7.1. = 7.0.0 - 2024-01-03 = * Add - Add Account Management tools with reset account functionality for partially onboarded accounts. * Add - Adding Compatibility Service to assist with flagging possible compatibility issues in the future. * Add - Add refund controls to transaction details view * Add - Add test mode notice in page order detail. * Add - Display a Confirmaton Modal on enabling Test Mode * Add - Introduce Customer currency, Deposit currency, Amount in Customer Currency and Deposit ID columns to the Transaction list UI and CSV export * Fix - Allow test phone number as Support Phone in Dev mode * Fix - Avoid using the removed deferred UPE flag * Fix - Ensure proper backfilling of subscription metadata (i.e. dates and cache) to the postmeta table when HPOS is enabled and compatibility mode (data syncing) is turned on. * Fix - Fetch and update the `_cancelled_email_sent` meta in a HPOS compatibile way. * Fix - fix: account currency hook return value * Fix - Fix account status error messages with links. * Fix - Fix country names with accents not showing correctly on international country fraud filter * Fix - Fix currency negative sign position on JS rendered amounts * Fix - Fixed a Level 3 error occurring during the capture of an authorization for amounts lower than the initial authorization amount. * Fix - Fixed Apple Pay Double Tax Calculation Issue * Fix - Fixed broken styles in authorization capture notifications * Fix - Fix incorrect amounts caused by zero-decimal currencies on Transactions, Deposits and Deposits CSV export * Fix - Fix missing customer data from transactions report * Fix - Fix missing order number in transaction reports CSV * Fix - Fix WooPay integration with AutomateWoo - Refer a Friend extension. * Fix - Improved error message for invalid payment method * Fix - Include discount fee in fees tooltip * Fix - Introduce WC_Payments_Express_Checkout_Button_Utils class. * Fix - Pass the pay-for-order params to get the pre-fetch session data * Fix - Prevents a PHP fatal error that occurs when the cart contains a renewal order item that no longer exists. * Fix - Resolved an issue that would cause undefined $current_page, $max_num_pages, and $paginate variable errors when viewing a page with the subscriptions-shortcode. * Fix - Revemoved pre-fretch session for button to prevent draft order creation * Fix - Update account balances on the Payments Overview screen when an instant deposit is requested * Fix - Update Qualitative Feedback note to have more efficient sql query. * Fix - When HPOS is enabled and data compatibility mode is turned on, make sure subscription date changes made to postmeta are synced to orders_meta table. * Fix - When using the checkout block to pay for renewal orders, ensure the order's cart hash is updated to make sure the existing order can be used. * Update - Actualized cards-related assets for settings and transactions pages. * Update - Cleanup the deprecated payment gateway processing - part II * Update - Cleanup the deprecated payment gateway processing - part III * Update - Confirmation when cancelling order with pending authorization. Automatic order changes submission if confirmed. * Update - Updates the anchor text for the fraud and risk tools documentation link on the Payments Settings page. * Update - Updates the behavior and display of the international IP address rule card if the rule is being affected by the WooCommerce core selling locations general option. * Dev - Add e2e tests for the currency switcher widget. * Dev - Added documentation for deposits REST API endpoints. * Dev - Bump WC tested up to version to 8.4.0. * Dev - Cleanup enqueueing of the scripts which were removed * Dev - Cleanup the deprecated payment gateway processing - part IV * Dev - Cleanup the deprecated payment gateway processing - part V * Dev - Cleanup the deprecated payment gateway processing - part VI * Dev - Comment: Fix declined 3DS card E2E test. * Dev - Deprecate the WC_Subscriptions_Synchroniser::add_to_recurring_cart_key(). Use WC_Subscriptions_Synchroniser::add_to_recurring_product_grouping_key() instead. * Dev - E2E test - Merchant facing: Multi-currency setup * Dev - Improve E2E checkout tests * Dev - Introduce a new wcs_get_subscription_grouping_key() function to generate a unique key for a subscription based on its billing schedule. This function uses the existing recurring cart key concept. * Dev - Remove "Set-up refund policy" Inbox note as superfluous. * Dev - remove unused factor flag for deferred UPE * Dev - Thank you page Tracks event * Dev - Updated subscriptions-core to version 6.6.0 = 6.9.2 - 2023-12-14 = * Add - Notice is added when merchant has funds that are not yet available for deposit. * Add - Show a deposit schedule notice on the deposits list page to indicate that future deposits can be expected. * Fix - Show deposit schedule message when deposits are unrestricted * Fix - Transactions List - indicate when a transaction is expected to be included in a future deposit = 6.9.1 - 2023-12-07 = * Fix - Display Klarna & Afterpay on the checkout for UK based stores = 6.9.0 - 2023-12-06 = * Add - Added cleanup code after Payment Processing - RPP. * Add - Adds new option to track dismissal of PO eligibility modal. * Add - Display an error banner on the connect page when the WooCommerce country is not supported. * Add - Filter to disable WooPay checkout auto-redirect and email input hooks. * Add - Handle failed transaction rate limiter in RPP. * Add - Handle fraud prevention service in InitialState (project RPP). * Add - Handle mimium amount in InitialState (project RPP). * Add - Introduce filters for channel, customer country, and risk level on the transactions list page. * Add - Store the working mode of the gateway (RPP). * Fix - Add AutomateWoo - Refer A Friend Add-On support on WooPay. * Fix - Add date_between filter for Authorization Reporting API. * Fix - Add invalid product id error check. * Fix - Allow Gradual signup accounts to continue with the Gradual KYC after abandoning it. * Fix - Allow requests with item IDs to be extended without exceptions. * Fix - Check that the email is set in the post global. * Fix - Display notice when clicking the WooPay button if variable product selection is incomplete. * Fix - Do not show the WooPay button on the product page when WC Bookings require confirmation. * Fix - Enable deferred intent creation when initialization process encounters cache unavailability. * Fix - Ensure express payment methods (Google and Apple Pay) correctly reflect eligible shipping methods after closing and reattempting payment. * Fix - Fixes a redirect to show the new onboarding when coming from WC Core. * Fix - Fix saved card payments not working on block checkout while card testing prevention is active. * Fix - Pass the pay-for-order params to the first-party auth flow. * Fix - Prevent merchants to access onboarding again after starting it in new flow. * Fix - Remove unsupported EUR currency from Afterpay payment method. * Fix - Show Payments menu sub-items only for merchants that completed KYC. * Fix - Support 'variation' product type when re-adding items to a cart. * Fix - When rendering customer reference in transaction details, fallback to order data. * Fix - When rendering customer reference on transaction details page, handle case with name being not provided in the order. * Update - Change PRB default height for new installations. * Update - Cleanup the deprecated payment gateway processing - part I. * Update - Correct some links that now lead to better documentation. * Update - Enable the new onboarding flow as default for all users. * Update - Exclude estimated deposits from the deposits list screen. * Update - Improvements to the dev mode and test mode indicators. * Update - Remove estimated status option from the advanced filters on the deposits list screen. * Update - Replace the deposit overview transactions list with a "transaction history is unavailable for instant deposits" message. * Update - Update Payments Overview deposits UI to simplify how we communicate upcoming deposits. * Update - Update to the new onboarding builder flow to not prefill country/address to US. * Dev - Add client user-agent value to Tracks event props. * Dev - Add E2E tests for Affirm and Afterpay checkouts. * Dev - Add E2E tests for checking out with Giropay. * Dev - Added customer details management within the re-engineered payment process. * Dev - Adds WCPay options to Woo Core option allow list to avoid 403 responses from Options API when getting and updating options in non-prod env. * Dev - Bump WC tested up to version to 8.3.1. * Dev - Fix a bug in WooPay button update Tracks. * Dev - Introduce filter `wcpay_payment_request_is_cart_supported`. Allow plugins to conditionally disable payment request buttons on cart and checkout pages containing products that do not support them. * Dev - Upgrade the csv-export JS package to the latest version. = 6.8.0 - 2023-11-16 = * Add - Added mechanism to track and log changes to the payment context (reengineering payment process) * Add - Add rejected payment method capability status * Add - Per-country amount limits for payment methods * Fix - Add Affiliate for WooCommerce support on WooPay. * Fix - Add WooCommerce Multi-Currency support on WooPay. * Fix - Allow customers using express payment methods (eg Apple Pay, Google Pay) to create an account automatically when purchasing subscription products if the store settings allow. * Fix - Display express payment buttons on checkout blocks pay-for-order page * Fix - Do not load WooPay button on external/affiliate product pages * Fix - Ensure shortcode renders for Privacy Policy & Terms of Service. * Fix - Fix builders being stuck after leaving KYC without finishing * Fix - Fix Multi-Currency formatting for totals on My Account > Subscriptions page. * Fix - Fix not allowed page when clicking Set up WooPayments from Core and account is already onboarded * Fix - Fix spelling of cancellation * Fix - Fix the amount conversion rate for blocked transactions on the transaction details page. * Fix - Fix total price for Google Pay on quantity change. * Fix - Fix transaction failure with UGX currency * Fix - Fix WooPay session handler's Store API route checks. * Fix - Handle checkout errors that appear during payment method creation request * Fix - Redact Stripe support contact prompt from error message when capturing amounts greater than authorized. * Fix - Remove references to In-Person Payments from Transaction settings when Cash on Delivery is disabled. * Fix - Show Google Pay/Apple Pay buttons in the Pay for Order page * Fix - Some array key info were not redacted in the logs * Fix - Update Fraud & Risk tools radio input background color to match the current theme. * Fix - Update plugin name to WooPayments * Fix - Updates to the account status logic to reflect status more accurately in some cases. * Update - Get WooPay 1st party auth flow to work on page load. * Update - Restructure the pay-for-order check * Update - Update links across the plugin from woocommerce.com to woo.com (new site URL). * Update - Update pay-for-order js config billing email to session email * Dev - Bump tested up to version for WP to 6.4 * Dev - Fire a tracks event for disputed order notice view. * Dev - Introduce `wcpay_terminal_payment_completed_order_status` filter. Allows overriding the order status after a successful terminal payment. * Dev - Remove outdated wcpay_transactions_download tracking events. * Dev - Remove unused JS code * Dev - Use automatic capture as default flag in new payment process. = 6.7.1 - 2023-11-03 = * Fix - Replaced the concrete logging class with a logger interface = 6.7.0 - 2023-11-01 = * Add - Added an internal logger class, meant for use by classes within src. * Add - Added Authorizations reporting endpoint. * Add - Added documentation for reports API * Add - Added WooPay button locations setting * Add - Add IE, DK, FI, NO, and SE as supported countries to Klarna payment method * Add - Integrate Duplicate Payment Prevention Service to the new payment process (project RPP). * Add - Introduce tracking for fraud purposes prior to WooPayments onboarding. * Add - Use admin theme color and the correct WooCommerce colors across subscription interfaces. * Add - Validate order phone number in InitialState (RPP). * Fix - Add additional security checks in the plugin * Fix - Capital loans page broken on WooCommerce 8.2.0 * Fix - Clear floats for payment request button wrapper. * Fix - Correct the display style for duplicate relevant notices in the thank-you page. * Fix - Ensure subscriptions have a date created that correctly accounts for the site's timezone. Fixes issues with subscriptions having a date created double the site's UTC offset. * Fix - Fix Afterpay checkout error when shipping information is missing * Fix - Fix Documents page loading on WooCommerce 8.2.0. * Fix - Fixed tooltip alignment for mobile view in payment settings * Fix - Fix Multi-Currency formatting on My Account > Orders page. * Fix - Fix order status inconsistency in HPOS mode on Order Edit screen. * Fix - Fix WooPay Same My Info adding unnecessary spacing to checkout * Fix - Format the display of created date for payment intent instead of timestamp. * Fix - Improve multi-currency compatibility with WooCommerce Deposits * Fix - Limit early WooPay session requests based on feature flag. * Fix - Prevent deleting wrong order in the duplicate payment prevention service * Fix - Prevent settings access to partially onboarded accounts * Fix - Prevent subscription from turning into manual renewal after deleting payment method. * Fix - Removed link to setup intent * Fix - Resolve an issue that would cause 3rd party plugin edit product fields with the show_if_variable-subscription class to be incorrectly hidden. * Fix - Set WooPay first party feature flag to off when incompatible extensions are active. * Fix - show/hide WooPay checkout page tooltip on click * Fix - Show loading state when accepting a dispute from the transaction details screen. * Fix - Update to properly show tooltip on Payments > Settings page when account is in PO state. * Fix - When HPOS is enabled, fix quick-editing the subscription statuses on the admin list table. * Update - Align deferred intent creation UPE checkout script with UPE inn terms of fonts appearance * Update - Enables deferred intent UPE for existing legacy card stores * Update - feat: add WooPay button SSR * Update - Unify payment method icon design * Update - Update Checkout appearance section to display WooPay checkout preview UI. * Dev - Added authentication required state * Dev - Add pay-for-order Tracks events * Dev - Add Shopper Tracks events * Dev - Bump WC and WP tested up to versions to 8.2.0 and 6.3 * Dev - Fixed version check regex * Dev - PHP 8.1 compatibility * Dev - Remove redundant Tracks events in WooPay OTP flow * Dev - Restore skipped e2e merchant tests * Dev - Update subscriptions-core to 6.4.0. = 6.6.2 - 2023-10-20 = * Fix - Fix PSR container conflicts * Update - Deprecate Sofort for any merchants who have not enabled it. Warn existing merchants about future deprecation. = 6.6.1 - 2023-10-19 = * Fix - Fix Documents page loading on WooCommerce 8.2.0. * Fix - Stripe Link eligibility at checkout = 6.6.0 - 2023-10-11 = * Add - Add a notice on the Settings page to request JCB capability for Japanese customers. * Add - Add current user data to the onboarding init request payload. This data is used for fraud prevention. * Add - Added API endpoint to fetch customer's saved payment methods. * Add - Added docs for cancel_authorization endpoint * Add - Added documentation for create payment intent API endpoint. * Add - Added documentation for payment methods API endpoint * Add - Add functionality to enable WooPay first party auth behind feature flag. * Add - Add helper function/method for raw currency amount conversion. * Add - Add Klarna payment method * Add - Add loading state to WooPay button * Add - Add payment intent creation endpoint * Add - Add the feature flag check for pay-for-order flow * Add - Add WC blocks spinner to the WooPay checkout styles. * Add - Behind a feature flag: dispute message added to transactions screen for disputes not needing a response. * Add - Display dispute information, recommended resolution steps, and actions directly on the transaction details screen to help merchants with dispute resolution. * Add - Display server error messages on Settings save * Add - Expand the data points added to the WooCommerce SSR to include all the main WooPayments features. * Add - Handle server-side feature flag for new UPE type enablement. * Add - Introduce the "Subscription Relationship" column under the Orders list admin page when HPOS is enabled. * Add - Show survey for merchants that disable WooPay. * Fix - Add Mix and Match Products support on WooPay. * Fix - Add multi-currency enablement check in WooPay session handling. * Fix - Comment: Behind a feature flag: Update documentation links (new/changed docs content) when notifying merchant that a dispute needs response. * Fix - Disable automatic currency switching and switcher widgets on pay_for_order page. * Fix - Ensure renewal orders paid via the Block Checkout are correctly linked to their subscription. * Fix - Ensure the order needs processing transient is deleted when a subscription order (eg renewal) is created. Fixes issues with renewal orders going straight to a completed status. * Fix - fix: save platform checkout info on blocks * Fix - Fix Apple Pay and Google Pay if card payments are disabled. * Fix - Fix error when disabling WCPay with core disabled. * Fix - Fix init WooPay and empty cart error * Fix - Fix modal header alignment on safari browser * Fix - Fix onboarding section on MultiCurrency settings page. * Fix - Fix WooPay express checkout button with product bundles on product page. * Fix - Hide tooltip related to Storefront theme in Multi-Currency settings when Storefront is not the active theme * Fix - Improved product details script with enhanced price calculation, and fallbacks for potential undefined values. * Fix - Improve escaping around attributes. * Fix - Load multi-currency class on setup page. * Fix - Missing styles on the Edit Subscription page when HPOS is enabled. * Fix - Only request WooPay session data once on blocks pages. * Fix - Payment method section missing for Affirm and Afterpay on transaction details page * Fix - Prevent charging completed or processing orders with a new payment intent ID * Fix - Prevent WooPay-related implementation to modify non-WooPay-specific webhooks by changing their data. * Fix - Prevent WooPay multiple redirect requests. * Fix - Redirect back to the connect page when attempting to access the new onboarding flow without a server connection. * Fix - Redirect back to the pay-for-order page when it's pay-for-order order * Fix - Resolved an issue that caused paying for failed/pending parent orders that include Product Add-ons to not calculate the correct total. * Fix - Speed up capturing terminal and authorized payments. * Fix - Store the correct subscription start date in postmeta and ordermeta when HPOS and data syncing is being used. * Fix - Tracking conditions * Fix - Virtual variable products no longer require shipping details when checking out with Apple Pay and Google Pay * Fix - When HPOS is enabled, deleting a customer will now delete their subscriptions. * Fix - When HPOS is enabled, make the orders_by_type_query filter box work in the WooCommerce orders screen. * Fix - WooPay save my info phone number fallback for virtual products * Update - Adapt the PO congratulations card copy for pending account status. * Update - Allow deferred intent creation UPE to support SEPA payments. * Update - Enhance design of bnpl payment methods status in settings screen * Update - Increase GBP transaction limit for Afterpay * Update - Only display the WCPay Subscriptions setting to existing users as part of deprecating this feature. * Update - Set WooPay First Party Authentication feature flag to default on. * Update - Store customer currencies as an option to avoid expensive calculation. * Update - Updated Transaction Details summary with added fee breakdown tooltip for disputed transactions. * Update - Update links that pointed to the dispute details screen to point to the transaction details screen * Update - Update Name Your Price compatibility to use new Compatibility methods. * Update - Update the content of modals that are displayed when deactivating the WooPayments or Woo Subscriptions plugins when the store has active Stripe Billing subscriptions. * Update - Update URL used to communicate with WooPay from the iFrame in the merchant site. * Dev - Added missing API docs links for payment intents and payment methods API endpoints * Dev - Capitalize the JCB label on transactions details page. * Dev - e2e tests for progressive onboarding * Dev - Extracting payment metadata and level 3 data generation into services. * Dev - Migrate away from hooking into actions in certain classes * Dev - Move fraud related service hooks out of class constructors and into new init_hooks methods. * Dev - Move hooks out of MultiCurrency constructor into own init_hooks method. * Dev - Refactored request class send() method * Dev - Refactor to move hook initialisation out of constructors. * Dev - This work is part of a UI improvements to increase disputes response that is behind a feature flag. A changelog entry will be added to represent the work as a whole. * Dev - Update subscriptions-core to 6.3.0. = 6.5.1 - 2023-09-26 = * Fix - fix incorrect payment method title for non-WooPayments gateways = 6.5.0 - 2023-09-21 = * Add - Add a new task prompt to set up APMs after onboarding. Fixed an issue where a notice would show up in some unintended circumstances on the APM setup. * Add - Add an option on the Settings screen to enable merchants to migrate their Stripe Billing subscriptions to on-site billing. * Add - Added additional meta data to payment requests * Add - Add onboarding task incentive badge. * Add - Add payment request class for loading, sanitizing, and escaping data (reengineering payment process) * Add - Add the express button on the pay for order page * Add - add WooPay checkout appearance documentation link * Add - Fall back to site logo when a custom WooPay logo has not been defined * Add - Introduce a new setting that enables store to opt into Subscription off-site billing via Stripe Billing. * Add - Load payment methods through the request class (re-engineering payment process). * Add - Record the source (Woo Subscriptions or WCPay Subscriptions) when a Stripe Billing subscription is created. * Add - Record the subscriptions environment context in transaction meta when Stripe Billing payments are handled. * Add - Redirect back to the pay-for-order page when it is pay-for-order order * Add - Support kanji and kana statement descriptors for Japanese merchants * Add - Warn about dev mode enabled on new onboarding flow choice * Fix - Allow request classes to be extended more than once. * Fix - Avoid empty fields in new onboarding flow * Fix - Corrected an issue causing incorrect responses at the cancel authorization API endpoint. * Fix - Disable automatic currency switching and switcher widgets on pay_for_order page. * Fix - Ensure the shipping phone number field is copied to subscriptions and their orders when copying address meta. * Fix - Ensure the Stripe Billing subscription is cancelled when the subscription is changed from WooPayments to another payment method. * Fix - express checkout links UI consistency & area increase * Fix - fix: save platform checkout info on blocks * Fix - fix checkout appearance width * Fix - Fix Currency Switcher Block flag rendering on Windows platform. * Fix - Fix deprecation warnings on blocks checkout. * Fix - Fix double indicators showing under Payments tab * Fix - Fixes the currency formatting for AED and SAR currencies. * Fix - Fix init WooPay and empty cart error * Fix - Fix Multi-currency exchange rate date format when using custom date or time settings. * Fix - Fix Multicurrency widget error on post/page edit screen * Fix - Fix single currency manual rate save producing error when no changes are made * Fix - Fix the way request params are loaded between parent and child classes. * Fix - Fix WooPay Session Handler in Store API requests. * Fix - Improve escaping around attributes. * Fix - Increase admin enqueue scripts priority to avoid compatibility issues with WooCommerce Beta Tester plugin. * Fix - Modify title in task to continue with onboarding * Fix - Prevent WooPay-related implementation to modify non-WooPay-specific webhooks by changing their data. * Fix - Refactor Woo Subscriptions compatibility to fix currency being able to be updated during renewals, resubscribes, or switches. * Fix - Update inbox note logic to prevent prompt to set up payment methods from showing on not fully onboarded account. * Update - Add notice for legacy UPE users about deferred UPE upcoming, and adjust wording for non-UPE users * Update - Disable refund button on order edit page when there is active or lost dispute. * Update - Enhanced Analytics SQL, added unit test for has_multi_currency_orders(). Improved code quality and test coverage. * Update - Improved `get_all_customer_currencies` method to retrieve existing order currencies faster. * Update - Improve the transaction details redirect user-experience by using client-side routing. * Update - Temporarily disable saving SEPA * Update - Update Multi-currency documentation links. * Update - Update outdated public documentation links on WooCommerce.com * Update - Update Tooltip component on ConvertedAmount. * Update - When HPOS is disabled, fetch subscriptions by customer_id using the user's subscription cache to improve performance. * Dev - Adding factor flags to control when to enter the new payment process. * Dev - Adding issuer evidence to dispute details. Hidden behind a feature flag * Dev - Comment: Update GH workflows to use PHP version from plugin file. * Dev - Comment: Update occurence of all ubuntu versions to ubuntu-latest * Dev - Deprecated the 'woocommerce_subscriptions_not_found_label' filter. * Dev - Fix payment context and subscription payment metadata stored on subscription recurring transactions. * Dev - Fix Tracks conditions * Dev - Migrate DetailsLink component to TypeScript to improve code quality * Dev - Migrate link-item.js to typescript * Dev - Migrate woopay-item to typescript * Dev - Remove reference to old experiment. * Dev - Update Base_Constant to return the singleton object for same static calls. * Dev - Updated subscriptions-core to 6.2.0 * Dev - Update the name of the A/B experiment on new onboarding. = 6.4.2 - 2023-09-14 = * Fix - Fix an error in the checkout when Afterpay is selected as payment method. = 6.4.1 - 2023-09-06 = * Fix - checkout processing when fields are hidden via customizer * Fix - Potential fatal error when viewing WooCommerce home because we try to check if store has been fully onboarded but account service is not yet initialized. * Fix - Resolved an issue with WCPay Subscription orders being set to failed during payment processing when Woo Subscriptions plugin is active. * Fix - Use the gateway from the Checkout class in case the main registered gateway isn't initialized for some reason. * Dev - Revert - Bump minimum required version of WooCommerce to 8.0.0 and WP to 6.1 * Dev - Setting the minimum required version of WooCommerce back to 7.8.0 and WP to 6.0 = 6.4.0 - 2023-08-31 = * Add - Added the Transactions reporting endpoint * Add - Adjust WooPay "custom message" to be reused for T&C and privacy policy * Add - Combine Session Initialization with User Authentication on WooPay. * Add - Enables deferred intent UPE for existing split UPE stores and newly onboarded stores. * Add - Onboarding flow state persistence * Fix - Adds consistent payment token labels for saved Stripe Link payment methods across my account, shortcode checkout, and blocks checkout pages. * Fix - Adds the possibility of continuing in progress onboarding process * Fix - Add `is_user_connected()` and `get_connected_user_data()` methods to `WC_Payments_Http_Interface` * Fix - Display onboarding MCC field validation error * Fix - Ensures that Stripe Link and SEPA Debit saved payment tokens are stored and validated with correct gateway IDs for relevant feature flags enabled. * Fix - Fixes subscription renewals with the UPE enabled. * Fix - Fix express checkout button design issues. * Fix - Fix phone number input widget on checkout page * Fix - Fix the WooPay logo so that it stays scaled up and visible on the WooPay button. * Fix - Fix zero decimal currency display in order notes * Fix - JavaScript is now correctly loaded on admin order screens when HPOS is enabled. * Fix - Prevent displaying "Fraud protection" menu on half-onboarded stores * Fix - Prevent fetching disputes on WooCommerce home task when store is not connected to a WooPayments account. * Fix - Prevent Progressive Onboarding accounts from adding APMs until completing full verification. * Fix - Provide per active discount Terms and Conditions link in the Account details card. * Fix - Remove precision overriding in multi-currency scenarios * Fix - Use domestic currency, instead of default deposit currency, to check if a payment method is compatible with the presentment currency in the checkout form. * Update - BNPLs: updated methods copy on settings page * Update - Change Payment_Intent_Status to Intent_Status * Update - Improve `Chip` component styles to with improved visual design and accessible contrast ratios. * Update - Removed wcpay_empty_state_preview_mode_v5 experiment code * Update - Set WooPay button default enabled for product/cart/checkout pages * Update - Updated copy for credit and debit card in settings * Update - Updated payment method tooltip in settings so that it is rendered correctly in mobile view * Update - Updated section "Payment Methods" in page "Settings" for mobile view * Update - Update express checkouts section in settings for mobile view * Update - Update tooltip styles to improve readability of interactive tooltip content. * Dev - Adding dispute object to DisputeDetails component * Dev - Adding HooksProxy and LegacyProxy to src, used to access code outside of it (hooks, globals, functions, and static methods). * Dev - Adding Psalm checks to function calls within `src`. * Dev - Add interface and concrete classes for Payment Method (project reengineering payment process). * Dev - Add LegacyContainer to `src` to allow loading classes from `includes`. * Dev - Add TypeScript development guidelines * Dev - Bump minimum required version of WooCommerce to 8.0.0 and WP to 6.1 * Dev - Fixing a mistake in the doc regarding the customer service. * Dev - Fix Tracks to record checkout view on all stores * Dev - Ignore updating currency precision if the country is Japan * Dev - Move Multi-Currency Order Meta Helper functionality behind url param. * Dev - Refactor the deposit status UI element to use the `Chip` component. * Dev - Track WooPay Save My Info checkbox usage = 6.3.2 - 2023-08-17 = * Fix - Revert fix WooPay Session Handler in Store API requests. = 6.3.1 - 2023-08-14 = * Fix - Fix AutomateWoo error on WooPay redirection. * Fix - Fix WooPay Session Handler in Store API requests. = 6.3.0 - 2023-08-09 = * Add - Add constant flag to use the new payment service (project reengineering payment process). * Add - Add JCB payment method coming soon notice * Add - Add payment service class (project reengineering payment process). * Add - Adds integration for Stripe Link while using split UPE with deferred intent creation. * Add - Add support for Japan * Add - Add support for the Enabled status from Stripe (will replace Restricted Soon in the case where there is no active deadline). * Add - Add support for United Arab Emirates * Add - Add Tracks events around account connect when promo is active. * Add - Add warning to the advanced settings page for WooPay incompatible extensions * Add - Add WooPay Gift Cards support. * Add - Add WooPay on onboarding payment methods. * Add - Add WooPay Points and Rewards support * Add - Allow WooPay verified email requests * Add - Ensure WooPay compatibility with the split UPE that has deferred intent creation implementation. * Add - Include WooPay merchant terms on WooCommerce Payment Methods settings. * Add - Prefill the Business Name and Country fields during WooPayments KYC. * Fix - Adding more descriptive error messages in gradual signup * Fix - Allow card gateway to load properly with WooPay enabled and subscription item in the cart. * Fix - Allow only domestic payments for BNPL payment methods * Fix - Allow only Japanese phone numbers for Japanese accounts * Fix - Avoid creating duplicate paid orders from a single payment intent. * Fix - Disputes listing column renamed to Respond by and will show empty when dispute does not need response or if respond by date has passed. * Fix - Enable customers who have migrated a WCPay subscription to a tokenised subscription to pay for existing failed orders. * Fix - Fatal error when using latest MailPoet with WooPay * Fix - Fixed the creation of the nonce that is sent to WooPay * Fix - Fix error while selecting product variations. Make the stripe payment messaging element load only if at least one BNPL method is active. * Fix - Fix extra requests when clicking WooPay express checkout button. * Fix - Fix Fraud and Risk Tools welcome tour to only show if Fraud and Risk banner learn more button is clicked and tour not previously dismissed. * Fix - Get WooPay adapted extensions from server * Fix - Highlight menu item when transaction details, deposit details, and disputes details page are opened. * Fix - Improve split UPE support of WooPay with multiple payment methods enabled. * Fix - Minor copy changes on the Set Up Real Payments modal. * Fix - Remove daily deposits for JP merchants as it's not permitted by our payment processor * Fix - Reverting change to the plugin name because of compatibility with iOS app. * Fix - Send correct shipping address to Afterpay in Classic Checkout * Fix - Send shipping address correctly to Afterpay in block checkout, when separate billing address is provided. * Fix - Update excerpt in readme.txt to improve ranking * Fix - Visual fixes for the Connect page hero. * Update - Allows nulls in min/max payment ranges definitions for UPE payment methods * Update - Minor copy fixes on the onboarding form. * Update - Modify 'Contact WooCommerce Support' badge located on the 'Payments accepted on checkout' section of Payments > Settings. * Update - Only show the post-onboarding congratulations message if user did not onboard in test mode. * Update - Unify payment method icon design * Dev - Add a dependency container for the new src directory. * Dev - Add generic Get_Request class, and migrate current simple requests to use it * Dev - Add PSR-4 autoloading for the src directory. * Dev - Add unit tests to cover WooPay button eligibility. * Dev - Add webpack script to generate RTL .css files and enqueue it for RTL languages * Dev - Adjust coding standards to align with WC Core. * Dev - Avoiding product-service exceptions during checkout, making debugging easier. * Dev - Fix Husky post-merge script to conform to `sh` syntax * Dev - Ignore updating currency precision if the country is Japan * Dev - Introduce model class WC_Payments_API_Setup_Intention for setup intents * Dev - Migrate certain WCPay shopper tracks to use wcpay prefix * Dev - Migrate Chip component to TypeScript to improve code quality. * Dev - Migrate DisputeStatusChip comp to TypeScript to improve code quality. * Dev - Pass tracks identity to WooPay iframe * Dev - Pass Tracks identity to WooPay to improve telemetry = 6.2.2 - 2023-08-01 = * Fix - Move the email title hook from the UPE class to the parent legacy gateway class, to avoid multiple callback invocations for the split UPE = 6.2.1 - 2023-07-31 = * Fix - Enhance query parameters validation in redirected requests. = 6.2.0 - 2023-07-19 = * Add - Add Android option in Device type advanced filter * Add - Add dispute notice to the WooCommerce order screen to highlight disputes awaiting a response. * Add - Added flag to allow us to remotely set if WooPay should be enabled or not for new merchants. * Add - Add tooltip and ARIA labels to payment method logos in transaction list * Add - Check for invalid extensions when one is activated or deactivated. * Add - Make Affirm and Afterpay Stripe messaging show up correctly for variable products, especially when we change variations. * Add - Prefill the store URL on the new Onboarding Form. * Add - Sending preloaded_requests to WooPay to avoid waiting for external requests * Fix - Add array_filter callback method * Fix - Added logic to check if the recurring cart array is present before displaying the recurring totals section in the cart. * Fix - Allow webhooks without livemode to be received. * Fix - Ensure when a customer changes the shipping method on cart and checkout that the recurring totals correctly reflect the chosen method. * Fix - Fix a fatal error on sites using WC Subscriptions versions below 4.0.0 * Fix - Fix Country informed as empty for logged-out user in the BNPL site messaging configuration. * Fix - Fixed typo in businessInfo strings in strings.tsx file * Fix - Fix fatal errors when get_product method returns null * Fix - Fix incorrect channel value in transaction description screen for Tap to Pay for Android transactions * Fix - Fix issue where subscription signup fees are not converted correctly with Multi-Currency. * Fix - Fix outdated documentation links. * Fix - Fix Save my info section style when Payment options is not numbered. * Fix - Remove duplicated payment method on thank you page when using WooPay. * Fix - Resolve an issue that prevented the "Used for variations" checkbox from being enabled on the variable subscription product edit screen on WC version v7.9.0. * Fix - Resolved an issue that caused the payment type metadata to not be included in payment requests. * Fix - Resolved errors that occurred when activating the WC Subscriptions plugin via bulk action on the WP plugins screen or updating the plugin via the WooCommerce Extensions screen. * Fix - Restore removed condition after naming convention. * Fix - Reverting change to the plugin name because of compatibility with iOS app. * Fix - When HPOS is enabled, permanently deleting a subscription related order wasn't updating the related orders cache properly. * Fix - Wrap list of payment method logos on next line * Update - Add incentive cache invalidation based on store context hash. * Update - Another chunk of branding rollout: update wordpress.org assets * Update - Check WCPay Subscriptions eligibility after store completes WooCommerce onboarding wizard. * Update - Confirm subscription being switched is owned by customer before trying to possibly use its currency to prevent error. * Update - Highlight the active dispute task if disputes are due within 72 hours. * Update - Improve disputes list page by hiding the "Disputed on" column by default, add an "Action" column with clear call to action button, highlight urgent disputes' due dates, and color code disputes' statuses. * Update - Mark an expired uncaptured order as 'Failed" instead of 'Canceled' * Update - Refactoring and cleanup of code * Update - Remove the Remind Me Later option from the Fraud and Risk Tools discoverability banner. * Update - Remove WooCommerce Payments from taking over the WC core settings page * Update - Simplify the active dispute task title when disputes are for multiple currencies to improve readability. * Update - Update WooCommerce Payments to WooPayments across the plugin * Update - WC Payments inbuilt subscriptions functionality is no longer enabled by default for eligible US based stores. * Dev - Add E2E tests for Fraud & Risk tools. * Dev - Adding a tracking property to record whether user went through the new onboarding UX or not. * Dev - Add tool to allow merchants to fix Multi-Currency exchange rates in orders. * Dev - Affirm&Afterpay: add new test cases to ensure the method availability on checkout * Dev - Affirm&Afterpay: refactor subscription products detection by using existing subs API * Dev - Extracting functionality for preventing duplicate payments into a service. * Dev - Fix tests by ensuring the rest_pre_dispatch filter returns a WP_REST_Response * Dev - Migrate `HorizontalList` component to TS * Dev - Removed an old flag for a feature which is now enabled for old users. Some refactoring of the task lists code (no impact on functionality). * Dev - Remove FRT E2E tests. * Dev - Update subscriptions-core to 6.0.0. = 6.1.1 - 2023-06-29 = * Fix - Fix syntax for advanced filters in WC 7.8 and over = 6.1.0 - 2023-06-28 = * Add - Add additional validation in VAT controller. * Add - Add Affirm and Afterpay to checkout block. * Add - Add Affirm and Afterpay to classic checkout. * Add - Add BNPL messaging to the product details page via Stripe Payment Method Messaging Element. * Add - Added a check to disable WooPay in case incompatible extensions are found. * Add - Adds implementation to handle deferred intent for the UPE in My account page. * Add - Adds support for UPE with deferred intent creation on the Blocks checkout page. * Add - Add Tap to Pay device type filter on transactions list page. * Add - Add usage tracking to WooPay express button location updates. * Add - Affirm and Afterpay logo support in transactions listing and transaction details. * Add - Connect page incentive for eligible merchants. * Add - Deposit schedule changes are disabled when an account has restrictions on it. * Add - Ensure Affirm and Afterpay available on checkout only when the payment is in expected range. * Add - Improve the wording and style of the "Active Disputes" task list item on the Payments → Overview screen to better communicate the urgency of resolving these disputes. * Add - Links in subscription deactivation modal open in a new tab. * Add - Show checkbox options for Affirm and Afterpay BNPL payment options. * Add - Update Affirm & Afterpay logos with border and icon images. * Add - Check for invalid extensions when one is activated or deactivated. * Fix - Add deadline and amount to clarify disputed order note. * Fix - Affirm&Afterpay: do not show messaging element for subscription products. * Fix - Allow `card_` prefix when validating payment method IDs to fix failing subscription renewals. * Fix - Check that a currency is available before adding it to the current currencies. Minor admin text string updates. Minor refactoring of MultiCurrency/RestController class. * Fix - Corrected bug where checkbox could not be enabled when credit card was disabled. * Fix - Fixed payment intents still getting confirmed on UPE when intent update fails. * Fix - Fix untranslated strings on the checkout page. * Fix - Fraudulent disputes will now show as Transaction unauthorized. * Fix - Hide Google Pay and Apple Pay buttons when total amount is zero on item details, cart, and checkout pages. * Fix - Improved user experience on onboarding form when validating fields if Enter key is pressed. * Fix - Improve the logic which determines if a user is activating the WC Subscriptions plugin when determining the need to load built-in subscriptions functionality. * Fix - Move WP hooks registration out of the core classes' constructors. * Fix - Remove all actions on preflight check. * Fix - Show descriptive dispute reasons in order notes. * Fix - Updated correct link for request classes docs. * Fix - Uses correct payment method title in order confirmation emails. * Update - Display the "Active Disputes" task list item on the Payments → Overview screen only if there are disputes due within seven days. * Update - Improve copy in Subscriptions deactivation modal. * Update - Improve the wording of the "Active Disputes" task list item on the WooCommerce → Home screen to better communicate the urgency of resolving these disputes. * Update - Moved the overview task list to the welcome greeting to improve visibility of important tasks. * Update - Update the design for UPE settings block. * Dev - Add support for Czech Republic, Hungary, and Sweden. * Dev - Bump minimum required version of WooCommerce to 7.8.0. * Dev - Comment: Add script to run QIT security tests locally. * Dev - Gracefully handle missing payment method constants. * Dev - minor refactor from js to tsx. * Dev - minor tsx refactor. * Dev - Tracking events for BNPL payment methods. = 6.0.0 - 2023-06-08 = * Add - Show Progressive Onboarding Express using Explat experiment * Fix - Add a session check to avoid fatal errors. * Fix - Add error notice when using ISK with decimals * Fix - Adjust style of deposits icon and text in account details for progressive accounts * Fix - Disabled subscription-related actions, webhooks, and invoice services on staging sites to avoid unintended interactions with the server and live site. * Fix - Ensures 3DS authenticated payment methods can be saved and reused with deferred intent UPE. * Fix - Ensures WCPay is present as default gateway on WC Settings screen when split UPE is enabled. * Fix - Fix account status chip from restricted to pending for PO accounts * Fix - Fixes pay for order page functionality in split and deferred intent UPE. * Fix - Fix for PO eligible request * Fix - Fix fraud and risk tools welcome tour copy to remove mentions of risk review. * Fix - Fix incorrectly labelled card gateway for the enabled deferred intent creation UPE * Fix - Fix single currency settings conversion preview for zero decimal currencies * Fix - Fix to an API route for Progressive Onboarding feature which was previously only used in development environment. * Fix - Just a few CSS updates for WooPay buttons. * Fix - Progressive onboarding design fixes * Fix - Resolves issue with multiple charge attempts during manual capture with UPE. * Fix - Show setup deposits task for PO accounts without sales and right after acount creation * Update - Connect account page design. * Update - Pass the setup mode selected during Progressive Onboarding to the server onboarding init. * Update - Update @wordpress/components to v19.8.5 * Dev - Behind progressive onboarding feature flag – Stardardize and send PO collected merchant info to server. * Dev - Fixes intermittently failing UPE E2E tests. * Dev - Fix failing e2e shopper test - WC Beta * Dev - Migrate DepositsStatus and PaymentsStatus components to Typescript * Dev - Remove fraud and risk tools feature flag checks and tests * Dev - Skip failing E2E refund tests. * Dev - Tracking for account balance section on the Payments > Overview page. * Dev - Update @woocommerce/components to v12.0.0 = 5.9.1 - 2023-06-05 = * Fix - Improve validation of WC analytics query filters * Fix - Improved validation of the order key arg when redirecting to subscription's change payment method URL. * Fix - Resolved an issue with customers being redirected to an incorrect Pay for Order URL after login. * Dev - Update subscriptions-core to 5.7.2 = 5.9.0 - 2023-05-17 = * Add - Adds the minimal functionality for the new Stripe payment flow that allows deferred payment/setup intent creation. The functionality is hidden behind the feature flag. * Add - Add support for 6 new countries in WCPay: Bulgaria, Croatia, and Romania * Add - Add support for complete_kyc_link wcpay-link-handler * Add - Disable the WooPay auto-redirect and SMS OTP modal in unsupported contexts. * Add - Enhanced fraud protection for your store. Reduce fraudulent transactions by using a set of customizable rules. * Add - Hide WooPay button in unsupported contexts * Add - PO fields data and controls, behind a feature flag. * Add - Support pending verification account status * Fix - Add fraud prevention token to cart and checkout payment request buttons * Fix - Check for the `AbstractCartRoute` class before making WooPay available. * Fix - Fatal error from third-party extensions using the `woocommerce_update_order` expecting the second parameter. * Fix - Fix AJAX response notice for multi-currency due to empty currencies data * Fix - Fixed blocks currency switcher widget * Fix - fixed php 8.1 wp-admin errors * Fix - Fix keyboard navigation for account balance tooltips on the Payments → Overview screen. * Fix - Handle WooPay requests using Store API cart token and Jetpack blog token. * Fix - Minor change for i18n - Remove extra trailing space in translation string, outside of the __ tag. * Fix - No longer display the Deposits card on the Payments Overview page for new merchants who don't have an estimated deposit * Fix - Prevent express checkout buttons from displaying when payments are disabled. * Fix - Prevent WooPay redirection when OTP frame is closed * Fix - Remove WooPay subscriptions user check endpoint. * Fix - Supply correct payment method instance to process_redirect_payment. * Fix - Usage tracking props when placing WooPay orders * Fix - Use timezone to check WooPay available countries * Update - Change from convention Platform Checkout to WooPay consistently. * Update - Handle incorrect address errors in terminal location API * Update - Refactor express payment method button display * Update - Remove the `simplifyDepositsUi` feature flag and legacy deposits UI code. * Update - Show a link to the documentation in the tooltip when the pending balance is negative. * Update - Update @woocommerce/experimental to v3.2.0 * Update - Update @wordpress/data-controls to v2.6.1 * Update - WooPay specific admin area usage tracking * Dev - Adds HTML coverage report for developer reference. * Dev - Add the 'wcs_recurring_shipping_package_rates_match_standard_rates' filter to enable third-parties to override whether the subscription packages match during checkout validation. * Dev - Behind progressive onboarding feature flag – Add PO eligibility modal. * Dev - Convert fraud protection settings related JavaScript files to TypeScript. * Dev - Enable third-party code to alter the delete payment token URL returned from flag_subscription_payment_token_deletions. * Dev - Explicitly mention gridicons and @wordpress/primitives as dev dependencies. * Dev - Pass the subscription object as the second parameter to `woocommerce_update_subscription` hook (and `woocommerce_update_order` for backwards compatibility). * Dev - Remove pinned composer version 2.0.6 from workflows * Dev - Resolve errors for third-party code using the URLs returned from WC_Subscriptions_Admin::add_subscription_url() and WCS_Cart_Renewal::get_checkout_payment_url() because they were erroneously escaped. * Dev - Return a response from the WC_Subscription::set_status() function in line with the parent WC_Order::set_status() function. * Dev - Run only pending timers to avoid recursive loop for AddPaymentMethodsTask tests. * Dev - Update @woocommerce/currency to v 4.2.0 * Dev - Update @woocommerce/date to v4.2.0 * Dev - Update @woocommerce/explat to v2.3.0 * Dev - Update @wordpress/api-fetch to v6.3.1 * Dev - Update @wordpress/babel-plugin-makepot to v4.3.2 * Dev - Update @wordpress/base-styles to v4.3.1 * Dev - Update @wordpress/block-editor to v8.5.10 * Dev - Update @wordpress/blocks to v11.5.3 * Dev - Update @wordpress/data to v6.6.1 * Dev - Update @wordpress/date to v4.5.0 * Dev - Update @wordpress/element dependency to 4.4.1 * Dev - Update @wordpress/hooks to v3.6.1 * Dev - Update @wordpress/html-entities to v3.6.1 * Dev - Update @wordpress/i18n to v4.6.1 * Dev - Update @wordpress/icons to v8.2.3 * Dev - Update @wordpress/jest-preset-default to v8.1.2 * Dev - Update @wordpress/plugins to v.4.4.3 * Dev - Update @wordpress/scripts to v19.2.3 * Dev - Update @wordpress/url to v3.7.1 * Dev - Update react-dom dependency to 17.0.2 * Dev - Update react dev dependency to 17.0.2 * Dev - Update subscriptions-core to 5.7.1 * Dev - Update version detection API for subscriptions-core * Dev - Update `@wordpress/dom-ready` to v3.6.1 * Dev - Usage tracking for deposits admin UI. = 5.8.1 - 2023-05-03 = * Fix - Fix WooPay express checkout button display issue on Cart blocks. = 5.8.0 - 2023-04-26 = * Add - Add additional fruad meta box statuses and messages. * Add - Add order failed hook * Add - Add settings page section anchors * Add - Clear account cache on update plugin * Add - Disable the WooPay auto-redirect and SMS OTP modal in unsupported contexts. * Add - Hide WooPay button in unsupported contexts * Add - New UI for Account Balances and Deposits on the Payments -> Overview admin screen, providing an enhanced user experience and a clearer account balance breakdown, upcoming deposit information, and deposit history. * Add - Prefill Stripe KYC with hardcoded data for test mode * Fix - Add check to prevent fraud & risk tools interacting with multi-currency if it is not enabled. * Fix - Added phone number length validation * Fix - Bump up the size of Capture action button on the Authorizations list page. * Fix - Check for the `AbstractCartRoute` class before making WooPay available. * Fix - Correctly determine subscription free shipping eligibility when the initial payment cart isn't eligible. Fixes erroneous "Invalid recurring shipping method" errors on checkout. * Fix - Handle WooPay requests using Store API cart token and Jetpack blog token. * Fix - Revert the added tests for Stripe Link due to rate limiting during concurrent execution. * Fix - Support subscription_variation product type when re-adding items to a cart. * Update - Improve how capture date deadline notice shows up in the details page * Update - Replace fraud status logic to use the same data from the fraud meta box. * Update - Update fraud transactions list to show manually blocked transactions in the correct tab. * Update - Update Jetpack dependencies, remove outdated ones, and fix failing tests. * Update - Updates legal mandate displayed prior to enabling WooPay. * Update - Updates the display of the remind me later button and changes the text of the dismiss button to 'dismiss' on the fraud and risk tools discoverability banner. * Update - Updates to the WordPress.org readme.txt file. * Update - Update the status column on the Payments > Deposits table to display as a Pill. * Update - Update the WooPay logo. * Update - Use WooCommerce Pill component to show risk levels in the transactions list. * Dev - Add payment_method_types array returned from Stripe payment_intent into WC_Payments_API_Intention object. * Dev - Add PHP unit tests for multiple files. * Dev - Bump minimum required version of WooCommerce to 7.4.1 * Dev - Bump WP (6.2) and PHP minimum version (7.2) * Dev - Change all the JS files to TSX files in the client/components/fraud-risk-tools-banner directory and its subdirectories. * Dev - Change is hidden behind feature flag, will have changelog added at later date * Dev - Expands the test coverage for the Stripe Link. * Dev - Fixed precision loss notice that occurs when running PHP 8.1. * Dev - Fix phpcs and semgrep warnings to improve code quality. * Dev - Migrate API get_account_data to use the core Request class. * Dev - Migrated refunds API endpoints to request classes * Dev - Migrate login_links, capital_links, tos_agreement to use the core Request class. * Dev - Migrate update_account API to the core Request class. * Dev - Remove the feature flag for custom deposit schedules, now enabled by default. * Dev - Replace DAY_IN_MS with new constant TIME.DAY_IN_MS in the task-list index file. * Dev - Update subscriptions-core to 5.6.0. * Dev - Update the WCPay Pill component to use WooCommerce Core's Pill component * Dev - Updating the multiline changelog entries to single line * Dev - Use `wp_safe_redirect()` when processing a payment method change request. = 5.7.0 - 2023-04-05 = * Add - Add ClickTooltip and HoverTooltip components to improve mobile UX when tooltip content is interactive. * Add - Add concurrency to workflows * Add - Added a confirmation modal when Order Status is changed to Cancel on Edit Order page * Add - Added Transaction Fee to order metadata and display it on Order admin screen. * Add - Add GitHub workflow to build live branch and inform the zip file to Jetpack Beta Builder. * Add - Add new fraud protection elements to the transaction details page. * Add - Add order handling and meta box updates for fraud and risk tools. * Add - Add order id support for payment details page. * Add - Add risk review and blocked tabs to transactions list page. * Add - Adds a filter to enable fraud settings. * Add - Adds fraud and risk tools banner to WCPay overview page. All fraud and risk tools, including this banner, are currently behind a feature flag. * Add - Add shopper IP country to fraud prevention metadata sent to the server * Add - Add store currency support for the price threshold fraud rule. * Add - Add welcome tour for fraud protection settings section. * Add - Advanced fraud protection level settings user interface * Add - Fraud protection settings data builder implementation * Add - Implement connection to the Intelligent Router and flow switching * Add - Introduce a Banner Notice component to be used in upcoming UI elements * Add - Prefill Stripe KYC data for non-progressive onboarding scenario * Add - Record the following events: wcpay_fraud_protection_banner_rendered, wcpay_fraud_protection_banner_learn_more_button_clicked, wcpay_fraud_protection_banner_remind_later_button_clicked, wcpay_fraud_protection_tour_clicked_through, wcpay_fraud_protection_tour_abandoned, wcpay_fraud_protection_risk_level_preset_enabled, wcpay_fraud_protection_advanced_settings_saved, wcpay_fraud_protection_transaction_reviewed_merchant_blocked, wcpay_fraud_protection_transaction_reviewed_merchant_approved, wcpay_fraud_protection_standard_modal_viewed, wcpay_fraud_protection_high_modal_viewed, wcpay_fraud_protection_advanced_settings_card_avs_mismatch_viewed, wcpay_fraud_protection_advanced_settings_card_cvc_verification_viewed, wcpay_fraud_protection_advanced_settings_card_international_ip_address_card_viewed, wcpay_fraud_protection_advanced_settings_card_international_billing_address_viewed, wcpay_fraud_protection_advanced_settings_card_address_mismatch_viewed, wcpay_fraud_protection_advanced_settings_card_price_threshold_viewed, wcpay_fraud_protection_advanced_settings_card_items_threshold_viewed, wcpay_fraud_protection_order_details_link_clicked * Add - Show Tap to Pay icon in transactions list page for Tap to Pay transactions * Fix - Check whether we have an instance of WC_Cart before invoking its methods on checkout * Fix - Fatal errors on the thank-you page due to the strong type check in our filters. * Fix - Fix bug with showing WC tasks when there is no WCPay account * Fix - Fix new user not being saved in platform * Fix - Fix the Request WCPay core class conflict with WP 6.2. * Fix - Fix WooPay phone number on blocks checkout * Fix - Prevent the WooPay Express button from showing up for pre-order products to be paid upon release. * Fix - Sort uncaptured transactions with oldest first. * Update - Add documentation links to fraud & risk tools. * Update - Adds basic protection level modal and fixes standard and high level protection modal rules * Update - Change the link in the Express checkouts setting. * Update - Change user-facing wording from payouts to deposits. * Update - Connection success notice. * Update - Improve Fraud & Risk tools layout and add missing UI elements. * Update - Replace custom tour component with TourKit from the WooCommerce components library. * Update - Replace international billing address with IP country filter on fraud and risk tools * Update - Update international filters to use selling locations settings instead stripe account country * Update - Update test mode notice for Authorizations tab * Update - Update the payment details page to reflect the Tap to Pay transactions channel * Dev - Adds UPE deferred intent feature flag for future development * Dev - Add userscript for WCPay Live Branch. * Dev - Bump minimum required version of WooCommerce to 7.3.0. * Dev - Made timezones consistent in tests by generating dynamically. * Dev - Optimize asset bundling and loading * Dev - Pass an instance of Platform_Checkout_Utilities when instantiating WC_Payments_Platform_Checkout_Button_Handler. * Dev - Refactor the `Tooltip` component to use TypeScript * Dev - Removed unused code from WC Payments API client class = 5.6.2 - 2023-03-23 = * Update - Security update. = 5.6.1 - 2023-03-20 = * Fix - Check whether we have an instance of WC_Cart before invoking its methods on checkout * Fix - Fatal errors on the thank-you page due to the strong type check in our filters. * Fix - Fix new user not being saved in platform = 5.6.0 - 2023-03-15 = * Add - Add a component that provides a tour experience. * Add - Add a notice for Norwegian merchants using WCPay to update WooCommerce to at least 7.5 * Add - Added WC_Payments_Customer_Service_API in WCPayCore * Add - Add Fraud Protection section to WCPay Settings screen. Section is hidden behind a feature flag DB option. * Add - Add WCPay support for CY, LU, DK, EE, FI, GR, LT, LV, MT, NO, SI, SK * Add - Display tap to pay fees in transaction timeline view * Add - Moved methods used for order metadata update to the Order Service class. * Add - Show PO todos in the Things To Do on WCPay overview page. * Fix - Added function exist check to avoid fatal error for undefined function. * Fix - Add merge queue trigger to the relevant workflows. * Fix - Appending the countries dropdown to body to prevent it being clipped * Fix - Fix bug with showing WC tasks when there is no WCPay account * Fix - Fix deposit schedule monthly anchor label when set to 'last day of the month'. * Fix - Fixes a problem where the Transactions table would have its layout modified when hovering over the currency conversion icon * Fix - Fixes focus change on gateway select with split UPE enabled. * Fix - Fixing the broken search on the Disputes page. * Fix - Fix WooPay request on stores with WooPay disabled * Fix - Prevent admin error notices being shown for the "subscription trial end" event that was caused by no callbacks being attached to this scheduled action. * Fix - Prevent auto-scroll on page load when WooPay is enabled. * Fix - Prevent WooPay and Link to be enabled at the same time * Fix - prevent WooPay modal from showing up when editing the theme via wp-admin * Fix - Re-enable split UPE E2E tests and ensure they pass. * Fix - Remove the recurring shipping method cache that caused bugs for third-party plugins like Conditional Shipping and Payments. * Fix - Replace PHP dependency myclabs/php-enum with a built-in solution due to conflicts with multiple PHP versions. * Fix - Right align cutomize button on the settings page * Fix - Use generated script dependencies to load script assets * Fix - WCPay Checkout JS is loaded for zero carts * Update - Log error message when calling redirect_to_login fails. * Update - Updated express payment method UI in settings page * Update - Update the deprecating set-output command * Dev - Add a feature flag for fraud and risk tools features. * Dev - Adding a centralized class to manage the gateway's mode (live/dev/test). * Dev - Fixes a flaky e2e test. * Dev - Made timezones consistent in tests by generating dynamically. * Dev - Release automation - Run smoke tests automatically after release zip is built. * Dev - Remove unused code for updating WCPay subscription dates. * Dev - Update GH workflow runner images from Ubuntu 18.04 to 20.04 * Dev - Update subscriptions-core to 5.5.0 = 5.5.1 - 2023-03-01 = * Add - When enabling WooPay, if legacy UPE is enabled, upgrades feature flag to split UPE instead. * Fix - Avoid rendering save cards checkbox for logged out users * Fix - Fix get woopay available countries return type * Fix - Fix handling saved tokens for payment gateways while using shortcode checkout * Fix - Fix subscription renewal creating multiple charges with UPE. * Fix - Fix WooPay settings notice visibility = 5.5.0 - 2023-02-22 = * Add - Added learn more link to deposits page * Add - Added tracking for the split UPE feature flag. * Add - Declare WooCommerce Payments compatible with High-Performance Order Storage. * Add - New support phone and email fields the general settings page. * Add - Pass settings fields of the plugins that use newsletter block to woopay. * Add - Pass the namespaces from the Store API checkout schema data to WooPay * Add - Pass the store's test mode value to WooPay requests to the OTP endpoint. * Add - The UPE is now the default checkout experience for newly onboarded merchants. It can sbe disabled with these instructions: https://woocommerce.com/?p=3337362#disabling * Fix - Add wp-i18n as split UPE script dependency to load split UPE elements. * Fix - Disable WooPay for unavailable countries * Fix - Display an error when the request for initiating the platform checkout fails. * Fix - External link accessibilty text style * Fix - Fixes Stripe Link compatibility with split UPE payment gateway * Fix - For stores using HPOS, ensure the originating subscription's currency is used when initiating a subscription switch. * Fix - Make sure available payment methods are provided for the automatic subscription renewals. * Fix - Point the "Learn more" link to a more appropriate document in the Apple Pay domain registration failure notification. * Fix - Re-enabled email triggered WooPay flow with Express Checkout flow. WooPay Express Checkout is currently behind a feature flag. * Fix - Remove unnecessary style dependency from WooPay checkbox. * Fix - Track user viewport and url when using WooPay * Update - Removed saved methods listing in My Account Add Payment Method page * Update - Updated express payment method UI in settings page * Update - Updated the Express checkout settings page * Update - WooPay CTA text in shortcode checkout * Dev - Adding a feature flag to allow further development of onboarding UX - currently this will have no effect on live stores. * Dev - Merge progressive onboarding prototype under a feature flag = 5.4.0 - 2023-02-01 = * Add - Add logging and order notes when WCPay Subscriptions are suspended or put on-hold. * Add - Highlight subscriptions with overdue payment in list view with red icon & tooltip. * Add - More context to the business details to show the actual message from Stripe, shows a modal if there is more than one error from Stripe. * Add - New wcs_set_order_address() helper function to set an array of address fields on an order or subscription. * Add - Skipping the email input step for WooPay express checkout flow when the email input field has already a value. The WooPay express checkout feature is behind a feature flag currently. * Fix - "Subscriptions by Payment Gateway" in WooCommerce → Status now shows the correct values when HPOS is enabled. * Fix - Add check for session before getting properties from session to avoid fatals in WooCommerce Subscriptions Multi-Currency compatibility. * Fix - Catch exceptions when changing payment method associated with a subscription to avoid fatal errors. * Fix - Check whether the order actually exists before accessing order properties in wcs_order_contains_subscription(). * Fix - Decode entities for the store name in the WooPay preview section. * Fix - Edit, add, and list Subscription admin pages now work when HPOS is enabled. * Fix - Fatal error when loading the Edit Subscription page with custom admin billing or shipping fields. * Fix - Fix ability to change currency when a subscription renewal is in the cart. * Fix - Fixed issues where multiple subscription purchases wouldn't appear on the My Account > Subscriptions screen on HPOS environments. * Fix - Fix TOS acceptance can prevent saving settings or accessing account dashboard * Fix - Fix WooPay redirection with express checkout when email is prepopulated. The WooPay express checkout is currently behind a feature flag. * Fix - Make the tooltip and fee description pill in Payments > Settings, show the correct Base rate when we have promotional rates applied. * Fix - Merge any custom meta_query args passed to wcs_get_orders_with_meta_query() to avoid overriding WC core args that map onto meta_query. * Fix - On HPOS environments, ensure subscription related order caches are updated when relationship order meta (eg `_subscription_renewal` or `_subscription_switch`) is updated. * Fix - On HPOS environments, update related orders cache when subscription is trashed, deleted, or restored / untrashed. * Fix - Prevent erroneously resyncing a subscription every time it is loaded from the database on HPOS environments. * Fix - Prevent initializing `Fraud_Prevention_Service` on contexts without session * Fix - Refactor `WCS_Meta_Box_Subscription_Data::save` to support HPOS stores, fixing a PHP warning notice when updating an order via the Edit Order screen. * Fix - Refactor `WC_Subscriptions_Renewal_Order` and `WC_Subscriptions_Tracker` classes to support HPOS stores. * Fix - Removed the potential for an infinite loop when getting a subscription's related orders while the subscription is being loaded. * Fix - Repair a potentially corrupted state of enabled payment method settings, that causes an error while saving settings. * Fix - Replace code using wp_count_posts(), get_post_type(), get_posts and wp_delete_post() with equivalent WC Data Store functions to support stores that have HPOS enabled. * Fix - Set the `download_permissions_granted` value when purchasing a downloadable subscription product when HPOS is enabled. * Fix - Shipping address correctly set when resubscribing to subscriptions that contains different billing and shipping addresses. * Fix - Updates to the styling of the task list title on the main account overview page to match the component default. * Fix - When a customer changes their address on their account or subscription, make sure the new address is saved when HPOS is enabled. * Fix - When a subscription's parent order is trashed or deleted, make sure the related subscription is also trashed or deleted on stores with HPOS enabled. * Fix - When a subscription is trashed or deleted, make sure it is cancelled first on stores with HPOS enabled. * Fix - When processing customer requests to update all their subscription payment methods, ensure the updated subscription is used to fetch the new payment meta, not and old instance. * Update - Add Apple Pay and Google Pay as supported payment methods in gateway descriptions. * Update - Enable WooPay Express Checkout button feature by default. * Update - Generate mandate only for orders using INR currency * Update - Show the cards payment method when using UPE and make it active and locked as per UPE requirements. * Dev - Add subscriptions-core library version to the WooCommerce system status report. * Dev - Allow loading chunks with JS concatenation enabled. * Dev - Fix failing unit tests due to spelling change in WooCommerce core. * Dev - Fix phpcs violations in the `WC_Subscriptions_Tracker` and `WCS_Admin_System_Status` classes to improve code quality. * Dev - Introduced a new `untrash_order()` in the `WCS_Orders_Table_Subscription_Data_Store` class to fix untrashing subscriptions on stores that have HPOS enabled. * Dev - Introduced a WCS_Object_Data_Cache_Manager and WCS_Object_Data_Cache_Manager_Many_To_One class as HPOS equivalents of the WCS_Post_Meta_Cache_Manager classes. * Dev - Moved the trash, untrash & delete related `add_actions()` in the `WC_Subscriptions_Manager` class to be added on the `woocommerce_loaded` action. * Dev - Remove deprecated `strptime` function in favour of `DateTime::createFromFormat`. * Dev - Skip running blocks E2E tests, add comment for the same. * Dev - unified express checkout settings * Dev - Update subscriptions-core to 5.2.0. * Dev - Update subscriptions-core to 5.3.1. = 5.3.0 - 2023-01-11 = * Add - Added support for WooPay express checkout on product page. This feature is currently behind a feature flag and is not yet publicly available. * Add - Add the processing order_id to WC Session and its handling to prevent duplicate orders * Fix - Ensure script dependencies are loaded properly even when trying to register them again in the same request. * Fix - Fix an issue which caused synced wcpay subscription to not sync the first payment date when the customer is charged upfront. * Fix - Fix subscriptions remaining on-hold after processing a WCPay Subscription renewal on HPOS environments. * Fix - Make the webhook processing respect the test/live mode setting for the gateway * Fix - Prevent 'No such customer' errors after store is migrated to a new WCPay account. * Fix - Prevent occasional fatal errors when creating customers via ActionScheduler jobs * Fix - Updates subscriptions' payment token when a new default payment method is set. * Update - Bail out before payment processing for WooPay's order validation request * Update - Check the status of previously initiated payments and mark orders as processing instead of initiating a new payment. * Update - Enhance save my info section on classic and Blocks checkout * Dev - Bump minimum required version of WooCommerce to 7.1.1. * Dev - Remove redundant compatibility utility class for admin notes. * Dev - Replace typed payment intent strings with constants saved in an Enum clas * Dev - Retry API requests on network failure if Idempotency-Key header is present. * Dev - The PR replaces hard-coded order status constants, with a dedicated Enum class to make it reusable across the codebase. = 5.2.1 - 2022-12-30 = * Fix - UPE not loading on checkout page due to undefined i18n. = 5.2.0 - 2022-12-21 = * Add - Add WooPay Express Checkout button iframe functionality. * Add - When deactivating WCPay plugin, warn merchant that active WCPay Subscriptions will continue collecting payment. * Fix - Allow the "Your payment information is incomplete." UPE checkout block message to be translated. * Fix - Extend Stripe Link availability check with available fees * Fix - Prevent `Additional Payment Methods` (UPE) checkboxes within `Payments > Settings` , from getting unchecked and checked automatically while selecting multiple items quickly. * Fix - Fix an undefined variable bug in WooPay Express Checkout. * Fix - Fix for an issue where a timestamp in the past could be displayed on the update business details task card. * Fix - Fix translations job from the post-release workflow * Fix - Fix wrong time displayed in the "You need to capture this charge before..." text on the payment details page. * Fix - Hide "capture charge" and "cancel authorization" actions on order details page when order status is processing or completed. * Fix - Make the tooltips on UPE settings ( Settings > Payments accepted on checkout ) , show the correct custom rates whenever applicable. * Fix - PR created by the code freeze workflow is now done by botwoo to allow PR checks to be triggered * Fix - Prevent checkboxes from getting unchecked automatically within the UPE onboarding form, when we click multiple selections within a very short time ( 1.5 seconds ) * Fix - Recurring payments for cards issued by Indian banks. * Fix - Remove the UPE notification badge on the Payments > Settings menu item * Fix - Set commit author as github-actions bot for the code freeze workflow * Fix - Updates subscriptions' payment token when a new default payment method is set. * Update - Update currency_rate endpoint to not make a request if currency_from value is not valid/expected value. * Dev - Bump minimum required version of WooCommerce to 7.0. * Dev - Remove unused pre-release workflow * Dev - Replace direct database queries which fetched orders with an invoice ID with an equivalent wc_get_orders() query * Dev - Replace direct database queries which fetched subscriptions with a wcpay subscription ID with an equivalent wc_get_orders() query * Dev - Show correct authorizations count when switching between live and test modes * Dev - Support custom REMOTE_PORT for xDebug = 5.1.2 - 2022-12-03 = * Fix - Import critical package instead of lazy loading. = 5.1.1 - 2022-12-02 = * Fix - Minor patch fix to cron functionality that does not appear to have front-end ramifications for customers. = 5.1.0 - 2022-11-30 = * Add - Add a counter of pending authorizations to Uncaptured tab in Transactions page. * Add - Added support for a WooPay express checkout button on checkout blocks. This feature is currently behind a feature flag and is not yet publicly available. * Add - Adds encryption to the exposed `client_secret` to harden the store against card testing attacks * Add - Add uncaptured transactions count badge to Transactions menu. * Add - Enable the improvements to the authorization and capture workflow, that were hidden behind a feature flag. * Add - Improve fingerprint mechanism on checkout page * Add - New wcs_get_orders_with_meta_query() helper function to query for orders and subscriptions. * Add - Send merchant's setting for ship_to_billing_address_only to platform checkout * Add - Support creating new platform checkout user from checkout blocks. * Fix - Fix an error in the Uncaptured transactions table when it is sorted using the Capture by column. * Fix - Fix blocks checkout when card testing prevention is active * Fix - fixed bug that would not allow customers to add new payment methods to WooPay * Fix - Fixed bug that would not allow customers using firefox to log in to WooPay sometimes * Fix - Fix the "Learn more" link on Express Payments section * Fix - Fix undefined element error on Cart block for WooPay enabled site. * Fix - Handle errors in retrieving a file gracefully * Fix - On HPOS stores, ensure payment tokens are copied from the subscription to the renewal order. * Fix - On HPOS stores, make sure the links in the related-orders table redirect to the new Edit Order URL. * Fix - On HPOS stores, when a subscription is loaded from the database, make sure all core subscription properties are read directly from meta. * Fix - On HPOS stores, when querying for subscriptions with wcs_get_orders_with_meta_query() with status 'any', ensure that wc_get_orders() queries for subscription statuses. * Fix - On HPOS stores, when saving a subscription make sure subscription properties (ie `_requires_manual_renewal`) are saved to the database. * Fix - Processing a manual renewal order with HPOS and data syncing enabled correctly saves the related order cache metadata on the subscription and prevents the post and order meta data getting out of sync. * Fix - Redirect modal not closing when customer clicks back button on safari * Fix - Refactor `WCS_Meta_Box_Schedule::save` to support HPOS stores, fixing a PHP warning notice when updating an order via the Edit Order screen. * Fix - Return a fresh instance of the renewal order after creating it. Fixes caching issues on HPOS sites where the returned order has no line items. * Fix - Set payment tokens when copying data between orders and subscriptions in a CRUD compatible way. Fixes PHP notices during renewal order process. * Fix - Update margin to fix cropping of search field for Multi-Currency currency search * Fix - Use botwoo user for a job in the post-release-updates workflow * Fix - Use supported CRUD apis to determine if subscriptions are present on store (`wcs_do_subscriptions_exist`) * Fix - When viewing My Account > Subscriptions, fix an issue where no subscriptions were listed when HPOS is enabled. * Fix - With HPOS and data syncing enabled, updating the status of a pending manual renewal order to a paid status correctly activates the related subscription. * Update - Display related orders table when viewing the new "Edit Order" page (HPOS enabled stores). * Update - Don't load WooPay scripts and styles when the WCPay payment gateway isn't available. * Update - Refactor our Related Orders data store classes (WCS_Related_Order_Store_Cached_CPT and WCS_Related_Order_Store_CPT) to use CRUD methods to support subscriptions and orders stored in HPOS. * Update - Refactor the `wcs_is_subscription` helper function to support HPOS. * Update - Replace instances of `get_posts()` across codebase with new wcs_get_orders_with_meta_query() function. * Update - Update copy of warning modal appearing while deactivating Subscriptions extension. * Dev - Add API docs for authorization endpoints * Dev - Add description for capture authorization endpoint * Dev - Add new workflow to amend (or generate) the changelog for the release * Dev - Add PHPCS `PEAR.WhiteSpace.ObjectOperatorIndent` rule. * Dev - Bump minimum required version of WooCommerce to 6.9 and WordPress to 5.9 * Dev - Fix section divider in Authentication API docs * Dev - Introduce a WC_Subscription::set_status() function to handle subscriptions set with a draft or auto-draft status. Replaces the need for the overriding WC_Subscription::get_status() which has been deleted. * Dev - Manual renewal orders created with HPOS and data syncing enabled are properly linked to the subscription by its `_subscription_renewal` meta and backfilled to posts table. * Dev - Refactor the saving of subscription dates in the subscription datastore to separate fetching changes and saving. Enables backfilling subscription dates when HPOS syncing is enabled. * Dev - Removed the deprecated "wcs_subscriptions_for_{$relation_type}_order" dynamic hook used to filter the list of related subscriptions for the given relation type. The following hooks have been removed with no alternative: wcs_subscriptions_for_renewal_order, wcs_subscriptions_for_switch_order, wcs_subscriptions_for_resubscribe_order * Dev - Show uncaptured transactions tab only when some specific criteria is met = 5.0.3 - 2022-11-15 = * Fix - Purchasing a synced subscription with WCPay Subscriptions correctly sets the next payment date to the sync date in Stripe. = 5.0.2 - 2022-11-14 = * Fix - Fixed rest api error for payment_gateways endpoint = 5.0.1 - 2022-11-10 = * Fix - Fix fatal error when non-admin access admin pages. = 5.0.0 - 2022-11-09 = * Add - Add capture authorization support from the list of authorizations * Add - Add capture authorization support from the payment details page. * Add - Added a Refund Confirmation modal on Edit Order screen status change * Add - Add endpoint to get platform checkout signature at time of request * Add - Add event when skipped the platform checkout * Add - Add Stripe Link set up inbox notification * Add - New data copier class to copy data to subscriptions and related orders in place of direct database queries in prepraration for HPOS support. * Add - New WCS_Orders_Table_Data_Store_Controller class to load the proper subscriptions data store when the store has HPOS enabled. * Add - New WCS_Orders_Table_Subscription_Data_Store class to support subscriptions stored in High-Performance Order Storage (HPOS). * Add - Pass the value of 'woocommerce_tax_display_cart' option from the merchant's store to WooPay * Add - Updated the wording in the balance component header and added a link to the settings page. * Add - Wire authorizations data to the transactions > uncaptured screen * Fix - Adjust regex to check the format of a tag for a pre-release in our GitHub workflow * Fix - Currency switcher block padding while editing it * Fix - Enable Link to support authorization/capture scenarios. * Fix - Fetch authorization data in payment details page only when the payment needs manual capture * Fix - Fixed error when visiting the plugins page * Fix - Fix platform checkout auto redirection for user with pre-populated email when they land on the checkout page. * Fix - Prevent proceeding to WooPay when in a preview context. * Fix - Update Stripe Link inbox notification wording. * Fix - When saving sync meta data on a new subscription, use 'woocommerce_new_subscription' instead of 'save_post'. This is to prevent errors when purchasing a subscription on stores that have HPOS enabled.[. * Update - Adjust texts and links in WC admin express checkout section. * Update - Hide upload buttons and minor UI improvements on submitted dispute form. * Update - Improve maybe_add_subscription_meta() and subscription_contains_synced_product() inside our WC_Subscriptions_Synchroniser class to use CRUD methods. * Update - Improve wcs_copy_order_address() to use modern APIs for setting address fields. * Update - Remove IE11 support. * Update - The subscription creation function `wcs_create_subscription` has been updated to use WooCommerce CRUD methods in preparation for supporting High Performance Order Storage (HPOS). * Update - Update priority for Multi-Currency filters for frontend prices and currency to later priority to avoid plugin conflicts. * Dev - Add filter to record wcpay server api response time * Dev - Add new GH workflow for post-release steps and improve formatting/naming in recent workflow files introduced * Dev - Add new GitHub workflows for release management (pre-release and release packages) * Dev - Add php unit tests watcher * Dev - Adds a new tracking event when the new KYC informational modal is opened. * Dev - Change mocked data in Authorizations store with actual data from API * Dev - Deprecated the "wcs_{type}_meta" dynamic hook used to filter data copied to subscriptions and renewal orders. Third-parties should use wc_subscriptions_{type}_data instead. * Dev - Deprecated the "wcs_{type}_meta_query" dynamic hook used to alter the database query used to fetch the meta data to copy between subscriptions and renewal orders. There is no direct replacement. Third-parties should use the "wc_subscriptions_{type}_data" or "wc_subscriptions_object_data" hooks instead. * Dev - Fix tests with WordPress 6.1 * Dev - i18n usage of strftime has been deprecated for subscription titles. Date is now formatted using woocommerce standard date formatting. * Dev - Refactor WC_Payment_Gateway_WCPay part 1 * Dev - Remove unnecessary babel plugins after IE11 support drop. * Dev - Replace the use of the deprecated wcs_renewal_order_meta hook with wc_subscription_renewal_order_data in the WCS_Related_Order_Store_Cached_CPT class. * Dev - Replace the use of the deprecated wcs_renewal_order_meta_query hook with wc_subscription_renewal_order_data * Dev - Switch to @woocommerce/dependency-extraction-webpack-plugin * Dev - Update a few workflows to use an env variable for the L-2 version of WC/WP * Dev - Update Docker image to wordpress:php7.4 so client dev environment runs with PHP 7.4 * Dev - Update Gridicons imports to be indivuals, reducing our bundle size. * Dev - Update husky to v 8.0.1. * Dev - Update lodash to version 4.17.21 * Dev - Update subscriptions-core to 2.5.1. * Dev - Update the deprecated interpolate-components npm package with the @automattic/interpolate-components package, and update to v1.2.1. * Dev - wcs_get_objects_property and wcs_set_objects_property have been marked as deprecated. Getters/Setters should be used on the objects instead. * Dev - woocommerce_new_subscription_data hook will only work with CPT datastore and so has been deprecated. = 4.9.0 - 2022-10-20 = * Add - Adds new notice and modal informing users about verifying their account during onboarding. * Add - Declare WooCommerce Payments incompatible with COT * Add - New Multi-Currency filter to display Analytics > Orders in customer currency. * Fix - Fix dropdown menu appearance in UPE payment methods when Gutenberg is active. * Fix - Fixed issue with Stripe rate limit during the checkout * Fix - Fix fatal error with call to MultiCurrency/Compatibility::convert_order_prices method. * Fix - Fix platform checkout store logo preview. * Fix - Move One Time Shipping metabox fields to use the woocommerce_product_options_shipping_product_data hook introduced in WC 6.0. * Fix - Prevent OTP modal from showing up when auto redirection is in progress. * Fix - Prevent WooPay OTP after click Place Order button * Fix - Save Link payment method tokens for subscription renewal * Fix - The ellipsis menu is now readable and we can dismiss the task in the "Things to do" task list from Payments > Overview page * Fix - Update default platform checkout host to pay.woo.com * Update - Improve handling of subscription bulk action execution. * Update - Update formatCurrency to decode HTML entities for rendering currency symbols. * Update - Update webhook processing to override existing meta data. * Dev - Add authorizations endpoints * Dev - Added Apple Pay and Google Pay to tags for the plugin. * Dev - Bump minimum required version of WooCommerce to 6.8 to support L2 policy * Dev - changed WooPay otp url * Dev - Split webpack configuration and enable HMR * Dev - Update E2E docker image and change image user * Dev - Update subscriptions-core to 2.3.0. = 4.8.1 - 2022-10-04 = * Fix - Fix fatal error thrown during the renewal order payment flow when the store doesn't have the WCPay Subscriptions feature enabled = 4.8.0 - 2022-09-29 = * Add - Add bundle size check for PR's. * Add - Allow subscription processing via WooPay. * Add - Auto redirect logged in platform checkout users. * Add - Remove deprecated beta headers from Stripe requests. * Add - Send a few extra pieces of data when checking if a WooPay user exists. * Add - StripeLink - prefill first and last names on checkout. * Add - Timezone formatting for transaction filters. * Fix - Ask for login when guest mode is disabled while checking out with WooPay. * Fix - Change 'zero-cost-fee' to 'empty-order', for product_code while sending level 3 data for shipping-only orders, without products. * Fix - Correct empty email error when StripeLink is active on checkout page. * Fix - Fix off by one error. * Fix - Fix the rate calculation when using Table Rate Shipping and per item or per line item calculation type. * Fix - Fix trial subscription checkout without WooPay signing up. * Fix - Hide button below email field at checkout, when StripeLink is disabled. * Fix - Hide Link payment method for non-us accounts. * Fix - Limit level 3 product code within 12 digits. * Fix - Prevent circumstance where WCPay Subscriptions customers could be double charged when the WC Subscriptions extension is active. * Fix - The feature flag for the task list in Payments > Overview page was not passed correctly. We now see the business details and reconnect wpcom user task when appropriate. * Update - Add timezone formatting only in case client provides user timezone. * Dev - Bump minimum required version of WooCommerce from 6.4 to 6.6. * Dev - Removes gutenberg plugin installation from E2E environment setup. * Dev - Update node to v16 and npm to v8. * Dev - Update to add E2E tests for the Multi-Currency functionality. = 4.7.2 - 2022-09-15 = * Fix - Fixes Order ID appearing as N/A in Payments > Transactions = 4.7.1 - 2022-09-13 = * Fix - Fix Apple Pay domain verify file missing error notice constantly displayed * Fix - Retain test mode context in CRON jobs queued up while checking out. = 4.7.0 - 2022-09-07 = * Add - Added meta to payment tokens used in subscriptions. * Add - Adding an authorization page part of the transactions view. Currently behind a flag and using mocked data. * Add - Adding support for WCA's Analytics and Multi-Currency when using custom order tables. * Add - Add support for getting a Stripe invoice. * Add - indicate setup-intent use in the request. * Add - Merchants can change their deposit schedule via the settings page. * Fix - Actualize FAQ link for 'Set up refund policy' inbox note. * Fix - Add customer ID to WP user during Store API checkout. * Fix - Add handling for guest user while updating customer with order data. * Fix - Analytics: Ensure the store default currency always displays in list. * Fix - Create WooPay user from trial subscription. * Fix - Dismissible country error message for Apple Pay. * Fix - Fix - Fatal Error caused in rare cases where quantity is zero during renewal, builds upon fix released in WC Pay 4.3.0. * Fix - Fix adding payment tokens for platform-created setup intents. * Fix - Fix deprecation notice for Automattic\WooCommerce\Blocks\StoreApi\RoutesController. * Fix - Fix error in updating subscription when saved cards are more than posts per page setting. * Fix - Fix file permission and merchant country errors for apple pay registration. * Fix - Fix Link errors after blocks plugin update. * Fix - Improvements to express checkout functionality: prevent errors on PHP 8 with empty product prices, and more percise taxes. * Fix - Remove duplication of deposit schedule on overview page. * Fix - Update id card to available payment method after disabling UPE. * Fix - Update WooCommerce Payments business details via "Things to do" task list leading to a blank page. * Fix - Upon losing a dispute, orders will no longer appear as processing, but as refunded instead. * Update - Modified query to get customer currencies when COT enabled. * Update - Modified usage tracking queries when COT enabled. * Update - Move the "Instant deposit" button on the Payments > Overview screen to the "Available balance" block. * Update - Only store a new token when paying for a subscription via WooPay if it doesn't exist already. * Update - Replaced direct DB query in oorders_with_charge_id_from_charge_ids with wc_get_orders. * Update - Replaced direct DB query in order_id_from_meta_key_value with wc_get_orders. * Update - The Payments > Overview "Temporarily Suspended" notice will only appear when deposits are "blocked". * Dev - Add new E2E workflow for pull requests & split existing tests into 3 jobs. * Dev - Bump minimum required version of WooCommerce from 6.2 to 6.4. * Dev - Bump minimum required version of WooCommerce in GH compatibility workflow from 6.2.2 to 6.4.1. * Dev - Minor readability change in tests. * Dev - Update E2E flows for subscription tests. * Dev - Update php-stubs/woocommerce-stubs to 6.8.0. * Dev - Update subscriptions-core to 2.2.1. * Dev - Update WC and Gutenberg versions in GH's oldest compatibility test. * Dev - Upgraded NodeJS version to 14. = 4.6.0 - 2022-08-18 = * Add - Adding support for payment request buttons (Apple Pay and Google Pay) to the Pay for Order page. * Add - Add transactions channel (In-Person or Online). * Add - Pass a parameter when creating an intention when the request comes from the platform checkout and it has a subscription. * Fix - Ask for login when buying a subscription with WooPay. * Fix - Avoid saving a session cookie when the currency is changed because of geolocation. * Fix - Check payment method before updating payment method title. * Fix - Fatal error when activating WooCommerce Subscriptions via WP-CLI when WooCommerce Payments is active. * Fix - Fix an issue while loading the Transaction Detail Page with py_ charge ids. * Fix - Fix compatibility issues with the new WooCommerce Blocks. * Fix - Fix error when changing subscription payment method via UPE checkout more than once in a session. * Fix - Not focusing email field when showing error message to prevent the autocompletion box from covering the error message. * Fix - Update currencies modal height. * Update - Make updating existing customer details during checkout async. * Update - Remove Charge request from Transactions Details page. The Charge data will be retrieved from the Payment Intent request. * Update - Update public WooPay link in registration copy. * Dev - Avoid execution context errors during E2E tests. * Dev - Bump minimum required version of WooCommerce from 6.0 to 6.2. * Dev - E2E GitHub Workflow: Re-run Failed Test Files. * Dev - Fixes E2E dispute test flow. * Dev - Force jest to use en_US.UTF-8 LANG. = 4.5.1 - 2022-08-08 = * Update - Security update. = 4.5.0 - 2022-07-27 = * Add - Add "Things to do" task list to the Payments Overview screen * Add - Add a task to the WooCommerce > Home screen notifying merchants of disputed payments that need a response. * Add - Add E2E test to measure checkout page performance * Add - Add redirect from charge ID to the payment intent ID equivalent in the transactions detail screen * Add - Adds support for filtering by customer currency in order analytics section * Add - Add support for filtering by multiple customer currencies in analytics * Add - Customer currency filter added to transactions page. * Add - Multi-Currency compatibility with Points & Rewards plugin. * Fix - Correctly show UPE payment methods when UPE is first enabled while manual capture is already enabled * Fix - Exclude blocks tests against incompatible WC versions + exclude specific WC versions for WP nightly tests * Fix - Fix a grammatical issue in the dispute task on the Payments > Overview screen when there is more than 1 dispute which needs a response. * Fix - Fix an issue with sorting by customer currency in Analytics > Orders * Fix - Fix caching issues after accepting a dispute. Resolves issues where the number of disputes needing a response doesn't update after accepting a dispute. * Fix - Fixed missing intent metadata in order * Fix - Fix for an issue where a console error relating to wcSettings displayed on WooCommerce > Settings page. * Fix - Shipping tax conversion while using Multicurrency. * Fix - Show the correct number of disputes needing a response in the Payments > Overview task list. * Fix - Show WooPay error message. * Update - Align Pricing display on Apple Pay/ Google Pay pop-ups with Cart * Update - Make adding fee breakdown to order notes async. * Update - Make updating saved payment method async. * Update - Move the “Things to do” task list to a more visible position on the Payments Overview screen. * Update - Redirect users to the disputes screen filtered to disputes which need a response when clicking on the Payments > Overview dispute task. * Update - Skip explicit currency format in admin area when no additional currencies are enabled, matching current fronted behaviour. * Update - Update transaction details link to use Payment Intent ID instead of Charge ID * Dev - Bump minimum required version of WooCommerce from 5.8 to 6.0 and WordPress from 5.7 to 5.8. * Dev - Included prelease version of WordPress into E2E tests * Dev - Tweak TypeScript definitions for Card readers as suggested on GitHub. * Dev - Use country-phone input component for terminal settings phone field = 4.4.0 - 2022-07-06 = * Add - Add handler for authenticated server links * Add - Add platform checkout order status sync webhooks * Add - Display a badge indicating the number of disputes which need a response in Payments > Disputes * Add - Disputes page: add a new filter option to the Show dropdown for displaying disputes awaiting a response. * Add - In Person Payments: Extend terminal intent creation to support payment_method_types, metadata, customer and capture_method parameters. * Add - Introduce StripeLink into WooCommerce blocks * Add - Support remote inbox notes with relative admin URLs * Fix - Fix payment methods in account after enabling Stripe Link * Fix - Hide Platform Checkout iframe on browser back button. * Fix - Platform Checkout settings responsiveness. * Fix - Use high-level order currency API for multicurrency subscription renewal orders (get_post_meta is not recommended for orders). * Update - Bump minimum required version of WooCommerce from 5.6 to 5.8. * Update - disable loader so that Stripe's skeleton loader is not used. * Update - Refactor WC_Payments_API_Intention to receive an instance of WC_Payments_API_Charge instead of multiple charge-related fields. * Dev - Include the WCPay version in the requests to the Platform Checkout * Dev - Update selectors & flow for dispute related tests = 4.3.0 - 2022-06-15 = * Add - Add ARN (Acquirer Reference Number) to refunds in payment details timeline. * Add - Add support for custom order numbers in addition to order IDs. * Add - record wcpay version in Platform Checkout Tracks events * Fix - Billing emails containing spaces. * Fix - Copy payment from a subscription to its renewal order when retrying failed renewal payment. * Fix - Dates presented in the "Respond by" column on the Payments → Disputes page are displayed in local time rather than UTC time. * Fix - Fatal Error caused in rare cases where a subscription line item's quantity is zero during renewal. * Fix - Fix default terminal location creation for store when blog name is empty. * Fix - Make hardcoded string in the checkout page translatable * Fix - Pass capture method preference to platform store * Fix - Preventing duplicate order notes and emails by clearing the cache before checking order status. * Fix - Verify domain with Apple Pay on websites using alternate folder structure. * Update - Add a new flag to conditionally display the Card Readers page when account has connected card readers. * Update - Bump minimum required version of WooCommerce from 5.4 to 5.6. * Update - Prevent expensive JOIN queries in Multi-Currency analytics if the store has never used Multi-Currency. * Dev - Add developer document for "Version Support Policy" * Dev - Update subscriptions-core to 2.1.0. = 4.2.1 - 2022-06-06 = * Fix - Add check to prevent fatal errors on checkout * Fix - Fix refunding of orders without _payment_method_id * Fix - Fix subscription renewal prices purchased in zero decimal based currencies like Yen = 4.2.0 - 2022-05-26 = * Add - Add a confirmation modal when enabling manual capture, and update UPE methods appearance if manual capture is enabled * Add - Fee details to order notes for successful payments. * Add - Introduced wcpay_test_mode filter to manipulate gateway test mode status * Add - Show WooPay Specific info on success page when customer paid with WooPay * Fix - Added support for new account status * Fix - Allow merchant to set store logo on Platform Checkout settings * Fix - Change type parameter with transaction_type for transactions url * Fix - Do not show country code on Platform Checkout opt-in. * Fix - Fixes fatal error on payment intent succeeded webhook. * Fix - Fix invalid_request_error when creating a payment with a negative unit_cost in level3 data * Fix - Fix store api url used by platform checkout to work on different permalink preferences * Fix - Fix the subscriptions onboarding modal and toast on newer WooCommerce versions (6.5.0+). * Fix - Pass store API mode to platform checkout session and endpoints. * Fix - Prevent fatal errors when fetching payment methods on the checkout block * Fix - Prevent sending empty values for required support email and phone fields. * Fix - Register draft order status hooks to stores with platform checkout enabled. * Fix - Update platform URL to pay.woo.com * Update - Bump minimum required version of WooCommerce from 5.2 to 5.4. * Update - E2E environment setup & workflow optimizations * Update - Enhance UPE survey. * Update - Modify the pointer content on the "Add new product" page when WooCommerce Subscriptions is not active. * Update - Refactor functions regarding timeline captured events for testing. * Update - Update KYC reminder email Tracks properties * Update - Update payment gateway method description * Update - Update session init request to platform checkout to use Jetpack Connection. * Dev - Deprecate the WC_Subscriptions_Order::get_meta() function. Use wcs_get_objects_property( $order, $meta_key, "single", $default ) instead. * Dev - In subscriptions-core source files, replace all cases of update_post_meta() where an Order ID is passed to use WC_Order::update_meta_data() instead. * Dev - In subscriptions-core source files, replace code using get_post_type( $order_id ) with WC Data Store get_order_type(). * Dev - In subscriptions-core source files, replace the get_post_meta() calls in WCS_Post_Meta_Cache_Manager with WC_Order::get_meta(). * Dev - Retrieving user subscription orders has been updated to use the WooCommerce specific APIs in WC_Subscriptions_Order. * Dev - Start using dart-sass for sass compilation by upgrading @wordpress/scripts package to 12.6.0 * Dev - Update subscriptions-core to 2.0.0. * Dev - Update the wcs_get_objects_property() function to prevent calls to get_post_meta() on objects that support calling the get_meta() function. = 4.1.0 - 2022-05-05 = * Add - Add documents and VAT invoices feature for supported countries. * Add - Adding StripeLink logo in the transactions list * Add - Add more logging info when sending requests to WooCommerce Payments server. * Add - Add StripeLink payment method in WCPay * Add - Moving email field on checkout page when StripeLink enabled. * Add - Send the blog locale to the server to enable server-side translations. * Fix - Ensure platform checkout SMS OTP iframe modal is always visible. * Fix - Fix an error in refunding In-Person Payments * Fix - Fix compatibility tests with Woo core 6.5. * Fix - Fixed clearing of stored payment methods when account is updated via webhook * Fix - Fixed issue with order tracking when mode is changed * Fix - Fixed redirect URL when user is already onboarded * Fix - Fix platform checkout eligibility check through ajax requests * Fix - Fix UPE alignment issues on classic checkout * Fix - Generate and add styles to UPE payment form on Blocks checkout * Fix - New KYC flow treatment mode issus with API calls and settings menu. * Fix - Prevent Stripe from sending email receipts for terminal payments * Fix - protect usage of account status to not break Payments > Overview page when account data is not defined * Fix - Replace enable toggle with account eligibility flag * Fix - Send receipt for Interac payments. * Fix - update _charge_id metadata to fix Refund button * Update - Card testing: rework card errors handling. * Update - Remove Stripe specific branding options from the card readers settings page. * Dev - Optimize E2E Setup to install Action Scheduler & WC Blocks from WordPress.org * Dev - Remove merchant onboarding E2E tests along with dependency. * Dev - Update currency rate cache mechanism * Dev - Updated documentation for REST-endpoints. * Dev - Update GitHub Actions E2E workflow to skip running WC Blocks tests against incompatible WooCommerce versions. = 4.0.2 - 2022-04-27 = * Add - Adds user's email to platform checkout request data. * Fix - Fixed non-working emails customization setting page caused by WCPay. * Fix - Fix missing customer_id on platform checkout * Fix - Inject Payments API into Blocks Package to remove dependency on the Blocks plugin for the platform checkout. = 4.0.1 - 2022-04-20 = * Fix - Fix templates folder missing in release package = 4.0.0 - 2022-04-13 = * Add - Add card readers business URL field browser validation. * Add - Add data fingerprinting for card testing prevention part of intent. * Add - Added handling for refund failure in payment timeline. * Add - Add fraud prevention section in the settings page, behind a feature flag. * Add - Add new merchant onboarding flow experiment to WCPay. * Add - Adds option to delete WC Refunds when WCPay refund fails. * Add - Add support for larger deposits export via async mail. * Add - Add support for larger disputes export via async email. * Add - Allow filtering API request params. * Add - Enable card readers section of WCPay admin area. * Add - Enable platform checkout if only no subscription product in cart. * Add - Force checkout refresh on fraudulent payment. * Add - In-Person Payments: Custom email for payment receipt. * Add - New connect account page design experiment. * Add - Record the event wcpay_kyc_reminder_merchant_returned in Tracks when visiting the URL for redirecting to Stripe. * Add - Send CVC confirmation to server for fraud prevention. * Fix - Add error notices when transactions cannot be retrieved. * Fix - Additional checks for domain verification file operations to prevent throwing Warnings on hosts that do not allow for suppression with `@`. * Fix - Card Readers: Preview receipt functionality. * Fix - Certain subscription renewal effects not executing for Subscriptions with WooCommerce Payments. * Fix - Empty file input to allow the user to select the same file again if there's an error. * Fix - Enable card readers branding section. * Fix - Enable WooCommerce Blocks checkout to hide option to save payment methods for a non-reusable payment method. * Fix - Using any other payment methods apart from WooCommerce Payments in "Pay for order" form triggers validation errors when UPE checkout is enabled. * Fix - Fix an error in refunding In-Person Payments. * Fix - Fixed the pricing displayed on Google Pay/ Apple Pay preview for variable subscription products. * Fix - Fix placeholders not being injected into the New Receipt email. * Fix - Fix printed receipt preview in Card Readers page not working on Firefox browser. * Fix - Fix saving UPE payment methods with WooCommerce Blocks checkout when a non-reusable payment method is enabled. * Fix - In-Person Payments: receipts are missing fees and shipping. * Fix - Prevent platform checkout iframe appear when go back. * Fix - Prevent refunding Interact payments (managed by Mobile apps). * Fix - Prevent Stripe from sending email receipts for terminal payments. * Fix - Style tweaks on checkout page for platform related elements. * Fix - Switch to global functions to remove deprecation warnings originating from WooCommerce Blocks. * Fix - Updates refund order note with reason format to fix failing E2E tests. * Update - Bump minimum required version of WooCommerce from 5.0 to 5.2. * Update - Enable platform-checkout tracking in stores without Jetpack plugin. * Update - Exclude Level3 data when capturing a terminal payment. * Update - Prevent webhook duplicate actions. * Update - Update Jetpack IDC package, and add two new options dynamicSiteUrlText and dynamicSiteUrlSupportLink. * Update - WooPay string and styling updates. * Dev - Account data caching improvements. * Dev - Deprecate the create customer endpoint. * Dev - Fixed bash warning when running tests locally. * Dev - Fix linter warnings in DepositsList. * Dev - Further migration of JavaScript components to TypeScript. * Dev - Refactor the KYC Optimisation to use the new Database_Cache class. * Dev - Skip e2e tests if WC version is 5.0.0 because of WooCommerce Checkout Blocks minimum WC Required version. = 3.9.3 - 2022-04-05 = * Fix - Payment Request Button - Do not set WC session cookie and HTML container on the product page if not enabled. = 3.9.2 - 2022-04-01 = * Fix - Fixing error related to some currencies = 3.9.1 - 2022-03-29 = * Fix - Fix single currency settings page error. = 3.9.0 - 2022-03-24 = * Add - Add compatibility between Multi-Currency and WooCommerce Name Your Price. * Add - Add wcpay_is_wcpay_subscriptions_enabled filter * Add - Add Webhook_Reliability service to fetch and process failed webhook events. * Add - Add `wcpay_metadata_from_order` filter which allows for injecting of arbitrary metadata and/or overriding of the `payment_context` * Add - Allow handling of previously paid for payment intents via the Checkout Store API * Add - Allow saving credit cards to platform account from classic checkout. * Add - Implement Tracks events to capture OTP usage * Add - New filter introduced: 'wc_payments_account_id_for_intent_confirmation'. * Add - Redirect merchants to the onboarding flow when a URL parameter is present * Add - Tracking for checkout start and completion * Add - Tracking for when platform checkout is offered * Fix - Add - when order data has no billing last name (ex. Google Pay payment) * Fix - Add all subscription line items not just subscription products when creating WCPay Subscriptions. * Fix - Check for variables before using them to prevent possible errors during UPE checkout process. * Fix - Don't anonymize new subscriptions related to old subscriptions via a resubscribe relationship * Fix - Do not show multi-currency inbox note until the merchant has set up a WCPay account * Fix - Fix bug when woocommerce_order_actions is called by a plugin or custom code. * Fix - Fix checkout as guest on platform checkout * Fix - Fixed failed order getting updated on successful payment in UPE * Fix - Fix Printed receipt endpoint throws error when called from Jetpack API * Fix - Fix the conversion from amount in the transactions list. * Fix - Fix the number formatting issues in Capital loans page loans list * Fix - Fix `is_platform_payment_method` flag on blocks checkout * Fix - Improve visibility of checkout fields for WCPay payment options on a darker theme * Fix - Load currencies from WooCommerce core. * Fix - Remove account business URL validation to allow values without http/s:// prefix. * Fix - Sets up subscriptions integration with the Mini Cart Block and adds new hook to filter compatible blocks. * Fix - Show currencies based on store country currency * Fix - Subscription token notices that appear on the My account > Payment methods page should be translatable. * Fix - update the menu count html tag for wp.com * Fix - When there is only one Shipping Method available in the recurring shipping package, make sure that this method is treated as selected in the current session and the `woocommerce_after_shipping_rate` action runs. * Update - Bump minimum required version of WooCommerce from 4.8 to 5.0. * Update - Fixes fee display that caused confusion for Merchants * Update - Remove feature flagged code that enable platform checkout inside UPE block. * Update - Update payment methods icons. * Update - Update transaction csv download emails to be sent to the current logged in admin. * Dev - Document usage of metadata generated from order * Dev - Do not enqueue admin assets if user cannot manage_woocommerce. * Dev - Refactor the processing part of Webhook Controller to a separate service. * Dev - Remove type "Tweak" from the list of changelog types. * Dev - REST API documentation * Dev - Skip e2e tests if WC version is 5.0.0 because of WooCommerce Checkout Blocks minimum WC Required version * Dev - Unit test support for PHP 8 and upgrade PHPUnit version to 9.5.14 * Dev - Updated contribution notes (how to add a changelog) = 3.8.2 - 2022-03-03 = * Fix - Fix fatal error when a subscription renews automatically. = 3.8.1 - 2022-03-03 = * Fix - Fix JavaScript error in blocks checkout and Customizer. = 3.8.0 - 2022-03-02 = * Add - Add a preview of uploaded logo and icon on terminal card reader receipt settings page. * Add - Add endpoint to retrieve a file content via API. * Add - Add jetpack-sync package to support Woo Mobile. * Add - Add jetpack-tracking module to track platform-checkout events. * Add - Add logic and UI to accept and see information about business loans, powered by Stripe Capital. * Add - Add merchant branding logo to print receipt. * Add - Add Preview printed version of IPP receipt to Card Readers settings page. * Add - Introduce `wcpay_payment_request_payment_method_title_suffix` filter. Allows plugins to replace "(WooCommerce Payments)" suffix on title of payment request buttons. * Fix - Conditionally add subscription payment gateway features. * Fix - Fix email download for deposit transactions. * Fix - Fix Stripe Level 3 data API limit when basket size is more than 200 items. * Fix - Styling issue in the subscription product publish modal. * Update - Add tracking for enabling and disabling platform checkout. * Update - Bump minimum required version of WooCommerce from 4.6 to 4.8. * Update - Enable capture terminal payment for succeeded intents. * Dev - Use Jetpack Changelogger to manage changelog files. * Tweak - Only add admin notes on non-AJAX requests. * Tweak - Refactor to add Order Service for updating order statuses. = 3.7.0 - 2022-02-10 = * Add - Filter by currency in disputes list * Add - Link to customer in disputes list * Update - Bump minimum required version of WordPress from 5.6 to 5.7. * Update - Bump minimum required version of WooCommerce from 4.5 to 4.6. * Add - Introduce sorting on disputes page. * Fix - Currency name not translated the Overview card title. * Add - Introduce advance filters on disputes page. * Add - UPE payment methods - BECS Direct Debit. * Fix - Missing currency field in disputes export file. * Add - Implement Jetpack Identity Crisis / Safe Mode banner. * Fix - Checkout with block-based themes. * Add - UPE payment method - EPS. * Fix - Replace uses of is_ajax() with wp_doing_ajax() in subscriptions-core. * Update - Improve handling of session data. * Fix - When changing the payment method, make sure the subscription total returns $0 when `subscriptions-core` is loaded after the `woocommerce_loaded` action hook. = 3.6.1 - 2022-01-27 = * Fix - Remove packages not compatible with PHP 7.0 * Update - Security update. = 3.6.0 - 2022-01-20 = * Update - Bump minimum required version of WooCommerce from 4.4 to 4.5. * Fix - UPE validation error visibility on checkout page. * Tweak - Load translations for js files directly from lang-pack json files. * Add - Add support for full transaction exports. * Fix - Simple subscription elements on the product edit page not shown/hidden when necessary. * Fix - Prevent fatal errors on the admin subscriptions screen when a subscription fails to load. * Fix - Compatibility issue when loading subscriptions templates. * Fix - Flag emoji rendering in currency switcher block widget * Fix - Error when saved Google Pay payment method does not have billing address name * Update - Update Payment Element from beta version to release version. * Fix - Ensure order is always associated with corresponding transaction in UPE Checkout. * Tweak - Display a more specific error message when a customer attempts to purchase a WCPay Subscription below the minimum transact-able amount. * Add - Add handling for payment_failed webhooks. * Add - Disputes pagination. * Add - Show a warning when attempting to create a subscription product with a price below the minimum amount. * Fix - When viewing a WCPay Subscription product page, make sure other gateway's express payment buttons aren't shown. * Fix - When viewing a WC Product page with a WCPay subscription product in cart, make sure other gateway's express payment buttons are shown. * Fix - Don't limit subscription products being created with an interval of more than one year when the WC Subscriptions plugin is active. * Fix - Subscriptions not renewing with subscription products that use a free trial period. * Fix - "Fees" column values are different in the downloaded CSV file for the transaction table = 3.5.0 - 2021-12-29 = * Fix - Error when renewing subscriptions with saved payment methods disabled. * Add - JS error boundaries to admin screens. * Update - Remove task from the overview list for setting up multiple currencies. * Update - Return to task "Set up payments" after finishing KYC from WC-Admin. * Fix - Improve race condition checks to prevent duplicate order status changes. * Fix - Explicit currency formatting in customer-facing emails. * Fix - Update tooltip wording when deleting product variation. * Fix - Remove references to WooCommerce Subscriptions extension in the tooltips found on the Payment Methods settings table. * Fix - Update the Automatic Recurring Payments column on the Payment Methods table to only show which payment methods are supported by WooCommerce Subscriptions Core. * Fix - Prevent deprecation warnings when purchasing subscriptions products using WooCommerce Blocks. * Tweak - Update recurring payments copy on payment gateways page. * Fix - Incorrect text when filtering subscriptions to no results. * Changed - Subscription products must have a recurring amount greater than $0. * Fix - Return correct product prices datatype in WCPay. * Fix - Stop errors when viewing Subscription details when purchased via SEPA Direct Debit. * Fix - Force currency check when preparing a payment intent to request even when is_admin() returns true. * Update - Bump minimum supported version of WooCommerce from 5.5 to 5.8. = 3.4.0 - 2021-12-08 = * Add - Allow UI customizations on checkout payment fields. * Add - Introduce `wcpay_payment_request_is_product_supported` filter. Allow plugins to conditionally disable payment request buttons on products that do not support them. * Update - Display hardware costs for the period in the transaction list with link to the details page * Fix - Incorrect customer links on Transactions page. * Fix - Incorrect prices in Payment Request Button for certain currencies. * Fix - Updates to fraud protection. * Add - Add support for suggested gateway methods in WC-Admin. * Fix - Prevent Payment Request buttons from showing up in Composite Product pages. * Update - Updated @woocommerce/experimental package to v2.1.0. * Add - Add support for suggested gateway methods in WC-Admin. * Add - Onboarding flows on the admin WooCommerce > Subscriptions screen for stores with no subscriptions yet. * Add - Card Reader receipt settings page. * Fix - Fatal error on thank you page for deleted orders. * Add - Error messages when dispute evidence exceeds Stripe limits. * Add - Export Disputes to CSV * Update - Remove "Boost your sales by accepting new payment methods" from the overview tasks list. * Fix - Onboarding must be completed before Subscriptions products can be published. * Fix - Show the prices in the correct currency when using the "All Products" block. * Add - Support business account branding settings. * Update - Capture order-related metadata not captured by mobile app for in-person payment transactions. * Add - REST endpoint to print IPP receipts. * Add - Deposit Status to Transaction export. = 3.3.0 - 2021-11-18 = * Add - Add Idempotency Key to POST headers. * Add - Add dispute order notes to Edit Order page. * Fix - Show a specific message instead of a generic one in the checkout block when non-UPE payment processing fails. * Update - Avoid having invalid intervals (greater than 1 year) in subscription products. * Update - The subscription fee label in the transaction timeline. * Update - Show red setup badge after 3 days instead of 7 * Add - Add compatibility between Multi-Currency and WooCommerce Bookings. * Add - Add compatibility between Multi-Currency and WooCommerce Pre-Orders. * Fix - Do not show default currency selector on Account Details page when only one currency is available. * Add - Add filters to disable or filter Multi-Currency sql query clauses for analytics. * Fix - Display risk for payment methods without risk assessment * Fix - Use configured domain instead of current domain for Apple Pay verification. * Fix - Fatal error when deactivating the WooCommerce plugin when WCPay Subscriptions is enabled. * Fix - Error where url parameters would get cleared on order-pay page if currency switcher block used. * Fix - Currency format on order-pay page if currency was changed via switcher. * Fix - Do not create WooCommerce Payments Subscriptions when using payment methods other than WooCommerce Payments. * Fix - Show proper payment gateway title on checkout load before updated by JavaScript. * Fix - Prevent a race condition leading to duplicate order paid statuses transitions. * Fix - 'payment_intent not found' errors when attempting to process the first invoice for a subscription. * Fix - UPE element not remounting on checkout update * Fix - Validate subscription product create and update args before submitting them to server. * Fix - Improve error messages when the minimum order amount has not been reached and allow fields to be displayed with less than the minimum amount. * Fix - Add consistent margins to the recurring taxes totals row on the Checkout and Cart block for subscription line items. * Fix - Fatal error due on subscription orders with no created date in order row template. * Fix - Fatal error on the customer payment page for subscription renewal orders with deleted products. * Fix - Misleading subscription order note on payment method change. * Fix - Incorrect error message when card ZIP validation fails. * Add - `Requires PHP` and `Requires at least` to the main plugin file. = 3.2.3 - 2021-11-01 = * Fix - Card fields on checkout not shown when the 'Enable payments via saved cards' setting is disabled. = 3.2.2 - 2021-10-29 = * Fix - Multisite compatibility - don't load subscriptions-core if already loaded by another multisite plugin. * Fix - Errors when attempting to get the WooCommerce Subscriptions Core version during PayPal requests. = 3.2.1 - 2021-10-28 = * Fix - PHP 7.2 compatibility - remove trailing commas from function args in subscriptions-core. = 3.2.0 - 2021-10-28 = * Add - Add subscriptions functionality via Stripe Billing and WC Subscriptions core. * Add - UPE track on upgrade and on setting toggle. * Fix - Prevent currency switcher to show when enabled currencies list is empty. * Fix - Show currency switcher notice until customer explicitly dismisses it. * Update - Switch the PaymentIntent ID and the Charge ID in the order notes and transaction details pages. * Fix - Track 'wcpay_payment_request_settings_change' for when updating the Payment Requests setting not being recorded. * Update - Fee breakdown when there's only a base fee * Fix - Inconsistent shipping options in Payment Request popup. * Fix - Payment methods checkbox UI looking off when Gutenberg is active. * Update - Remove unused "wcpay_deposits_summary_empty_state_click" track. * Fix - Border style not being applied properly on Multi-Currency block widget. * Fix - Applied sentence case on all strings * Fix - Missing customer information after guest checkout via Checkout Block. * Fix - Show correct payment method name during checkout using upe methods. * Fix - Multi-Currency settings rounding option and preview. * Fix - Payment failure on checkout block with UPE when phone number field is hidden * Update - Adds a scheduled action which makes updating the account cache more efficient * Add - Add compatibility between Multi-Currency and WooCommerce UPS shipping extension. * Add - Early access: allow your store to collect payments with SEPA Direct Debit. Enable the feature in settings! * Add - Add compatibility between Multi-Currency and WooCommerce FedEx shipping extension. * Fix - Fix decimal error with shipping calculations with Multi-Currency. * Add - Add support for float type values for quantity. * Fix - Allow payment_intent_succeeded webhook to handle orders without intent_id attached. * Add - Add compatibility between Multi-Currency and WooCommerce Product Add Ons version 4.3.0 and higher. * Add - Enable Bancontact UPE method. * Add - Enable P24 UPE method. * Add - Enable iDeal UPE method. * Add - Payment method activation requirements modal and API integration. * Add - Add state handling for UPE methods for better merchant notification on what methods are able to be used. * Fix - Order currency incorrect if new user/customer created during checkout. * Fix - Validation now works when adding a new payment method, or paying for an order. = 3.1.0 - 2021-10-06 = * Fix - Issue affecting analytics for Multi-Currency orders made with a zero-decimal to non-zero decimal conversion. * Add - Customer Multi-Currency onboarding flow. * Add - Checkbox toggle for disabling customer Multi-Currency feature in Advanced Settings. * Add - Update layout of the Multi-Currency settings screen. * Fix - Fixed missing file error for removed CSS file. * Add - Currency deletion confirmation modal for currencies that are bound to an UPE method. * Fix - Currency switcher does not affect order confirmation screen prices. * Fix - Error when attempting to change the payment method for a subscription with UPE enabled. * Add - Multi-Currency track currency added. * Fix - Fill missing order_intent_info even if an exception occurs. * Fix - Authorize and capture payments later with new credit cards. * Add - Gutenberg Block Widget for Multi-Currency. * Update - WCPay logo. * Fix - Translations in transaction/deposit exports * Fix - Update shipping cost in payment sheet when changing payment method. * Fix - Transaction search with translated terms. * Update - Replace REST endpoint for onboarding initialization. * Fix - UPE missing international card fees. * Update - Bump minimum supported version of WooCommerce from 5.4 to 5.5. * Update - Bump minimum required version of WooCommerce from 4.0 to 4.4. * Fix - Add credit card on My Account using other payment gateways does not show "Your card number is incomplete" error. * Update - Continue loading WCPay if the account is connected. * Add - Message to suggest using the previous version of WooCommerce Payments for old Woo core versions. * Fix - Appearance of upload file buttons inside challenge dispute page. * Fix - Enable logging for UPE checkout errors. * Update - Composer package `automattic/jetpack-connection` from v1.20.0 to v1.30.5. = 3.0.0 - 2021-09-16 = * Add - Download deposits report in CSV. * Fix - Use store currency on analytics leaderboard when Multi-Currency is enabled. * Add - Add API to expose Multi-Currency widget to theme/plugin developers for easy integration. * Fix - Enabled currencies modal UI. * Fix - User order currency format on admin refund button. * Fix - Clear the list of selected currencies after closing the modal for adding currencies. * Fix - Fix subscription change payment method errors after entering a payment method that fails. * Fix - Prevent duplicate account onboarding requests. * Fix - Filter out merchant-facing payment errors from customer error notices. * Fix - Add primary action to high priority tasks. = 2.9.1 - 2021-09-07 = * Fix - Error while checking out with UPE when fields are hidden. * Fix - Unable to onboard when in treatment mode. = 2.9.0 - 2021-08-25 = * Add - Split discount line in timeline into variable fee and fixed fee. * Add - Order status validation for payments/orders/{order_id}/create_customer API. * Add - Add country code parameter to ExPlat API requests. * Add - Add a new hook to get a list of enabled payment request methods. * Fix - Align table items according to design correctly. * Fix - Fatal error if wcpay_multi_currency_enabled_currencies is a string. * Fix - Show the estimated deposit date in the transactions CSV export rather than the deposit ID. * Fix - Keep track of customer id in non logged in users. * Update - Bump minimum supported version of WooCommerce from 5.3 to 5.4. = 2.8.4 - 2021-08-17 = * Fix - Fix database connection error on account cache clear. * Fix - Fix fatal error logged when updating analytics data when account is not connected to Stripe. * Fix - Multi-Currency Compatibility fatal error with Subscriptions when account is not connected to Stripe. = 2.8.3 - 2021-08-10 = * Fix - Fix for payment request buttons when the new payment methods gateway is enabled. = 2.8.2 - 2021-08-05 = * Fix - If account is disconnected or not set up do not display onboarding task and UPE inbox note. * Fix - Fix for the site acting as disconnected after the account cache expires. * Fix - Fix for failed Giropay and Sofort transactions causing an error. = 2.8.1 - 2021-08-04 = * Fix - Enable Multi-Currency only if there is a linked WooCommerce Payments account. = 2.8.0 - 2021-08-04 = * Add - Allow merchants to add additional currencies to their store, allowing a store’s customers to shop and browse in the currency of their choice. * Add - *Early access*: allow your store to collect payments with Giropay and Sofort. Enable the feature in settings! * Add - Use date picker for applicable dispute evidence fields. * Fix - Avoid crash when seeing the details of an empty deposit. * Fix - Disabled Payment Request Buttons when order has to be split into multiple packages because Payment Requests do not support that use case. * Fix - Fee discounts should use the discount currency rather than the base fee currency. * Fix - Do not redirect to the onboarding page when account retrieval fails. * Add - Allow the customer to perform SCA authentication on Subscription renewals. * Update - Actualized supported countries list for onboarding. * Add - Dispute Status Chip into the header of the Dispute Details page. * Fix - Use a singular label in the summary of Transactions and Deposits lists. * Add - Disable payment gateway when not in test mode and not using https or ssl checkout enforcement. * Fix - Improved errors handling during onboarding and page overview. * Update - Remove Account in the old Settings page. * Update - Bump minimum supported version of WooCommerce from 5.2 to 5.3. * Update - Bump minimum supported version of WordPress from 5.5 to 5.6. * Fix - Stop refund process when using an invalid amount * Fix - Improve sanitization of ExPlat cookie. * Add - Show fee breakdown in transaction details timeline. * Add - REST endpoint to get customer id from an order. * Fix - Explat not caching when no variation is returned. = 2.7.1 - 2021-07-26 = * Fix - Ensure test mode setting value is correctly saved. * Fix - Onboarding redirection occasionally not finalizing account connection. = 2.7.0 - 2021-07-14 = * Add - Add a link to the snackbar notice that appears after submitting or saving evidence for a dispute challenge. * Add - Support saving new cards and paying with previously saved cards in the WooCommerce Checkout Block. * Fix - WooCommerce Payments admin pages redirect to the onboarding page when the WooCommerce Payments account is disconnected. * Fix - Do not overwrite admin pages when account is disconnected. * Update - Set a description when creating payment intents. * Add - Add dispute resolution task. = 2.6.1 - 2021-07-01 = * Fix - Updates the notes query filters to prevent breaking the WooCommerce > Home inbox. = 2.6.0 - 2021-06-23 = * Add - Notify the admin if WordPress.com user connection is broken. * Add - Experimental PHP client for Explat. * Add - WooCommerce Payment inbox notifications to the overview screen. * Fix - Fix fatal error if store currency is changed after enabled (multi) currencies set. * Fix - Use of deprecated call-style to registerPaymentMethods. WooCommerce Payments now requires WooCommerce Blocks of at least version 3.9.0. * Fix - Deposit date on Transactions list page. * Fix - Rounding error when displaying fee percentages on the Overview and Transactions pages. * Add - Error message when total size of dispute evidence files uploaded goes over limit. * Update - Pass currency to wc_price when adding intent notes to orders. * Update - Instant deposit inbox note wording. * Fix - Deposit overview details for non instant ones. * Add - Introduce new settings layout * Update - Removed "Branded" and "Custom label" options on Payment request buttons to align with design guidelines. * Update - Converted payment request button size value to distinct options to align with design guidelines. * Tweak - Run post-upgrade actions during any request instead of only on wp-admin requests. * Update - Payment request button should guide users to login when necessary. * Add - When setting WooCommerce Payments up, inform if merchant business country is not supported. * Update - Bump minimum supported version of WooCommerce from 4.8 to 5.2. * Add - Introduce advance filters on deposits page. * Update - Prefill OAuth flow with WC store country. = 2.5.0 - 2021-06-02 = * Fix - Fix hover dialog for close button on modals, unify styling and layout of modal buttons. * Update - Use Site Language when rendering Stripe elements. * Update - Use blog ID for authenticating most of the requests. * Fix - Misaligned columns on Deposits page. * Add - Tracking for returning from OAuth connection. * Fix - Transactions and deposits counts on the table summary are rendered as "undefined". * Update - Deposit overview details. * Add - Redirect to WooCommerce home page after successful WooCommerce Payments KYC (Know Your Customer). * Fix - Added CSV column heading for transaction id column. * Update - Bump minimum supported version of WordPress from 5.4 to 5.5. * Update - Bump minimum supported version of WooCommerce from 4.5 to 4.8. * Add - Deposit overviews have been added to the overview page. * Update - Account overview page is now GA and default page for woocommerce payments. * Update - Base fee and account status has been moved to overview page from WCPay settings. * Fix - Express payment method being displayed on blocks checkout when Payment Request is not supported. * Fix - Subscription sign-up fees not included in total for Payment Request Button. = 2.4.0 - 2021-05-12 = * Update - Improve the Connect Account page. * Update - Base UI components and their styling. * Fix - Deposits overview details not displayed. * Fix - WooCommerce Payments disappeared from WooCommerce Settings if WooCommerce Subscriptions is activated. * Add - REST endpoint to capture payments by order ID. * Add - Explat package for A/B tests. * Add - Payment request button support for checkout and cart blocks. * Update - Bump minimum supported WooCommerce version from 4.0 to 4.5. * Update - Implement expirement on Connect Page. * Fix - Columns are misaligned on Payments->Transactions/Disputes page. * Fix - Risk level is displayed as a "Numeric" value in transactions CSV. = 2.3.3 - 2021-05-06 = * Update - Additional logic and styling for instant deposits. = 2.3.2 - 2021-04-27 = * Fix - Error when purchasing free trial subscriptions. = 2.3.1 - 2021-04-26 = * Fix - Various account connection cache tweaks * Update - Use option instead of transient for caching account data * Fix - Error when using SCA / 3DS credit card in checkout block. = 2.3.0 - 2021-04-21 = * Add - Introduced deposit currency filter for transactions overview page. * Add - Download transactions report in CSV. * Update - Tweak the connection detection logic. * Add - Notification badge next to payments menu. * Fix - Fixed broken search on transactions list page. * Add - More helpful message on checkout errors. * Update - Change the default track `recordEvent` to use @woocommerce/tracks. * Add - WPCOM connection status event prop to 'wcpay_connect_account_clicked' track. * Add - Allow users to clear the account cache. * Update - Bump minimum supported version of WordPress from 5.3 to 5.4. * Add - Add a new admin note to collect qualitative feedback. * Add - Introduced deposit currency filter for deposits overview page. * Update - Make Payment Request Button available for all merchants. * Add - Configurable Payment Request Button locations. * Add - Addition of the Instant Deposits feature to allow qualified merchants to manually trigger payouts. = 2.2.0 - 2021-03-31 = * Fix - Paying with a saved card for a subscription with a free trial will now correctly save the chosen payment method to the order for future renewals. * Add - Payment Request Button support for US merchants (Apple Pay, Google Pay, Microsoft Pay, and the browser standard Payment Request API). * Update - Not passing level3 data for non-US merchants. * Add - REST endpoint for fetching account data. * Add - Deposits list pagination and sorting. * Fix - Deposit overview now displays placeholder information instead of continuing to load when an error happens. = 2.1.1 - 2021-03-23 = * Fix - Fatal error when a subscription is processed with action scheduler hook. = 2.1.0 - 2021-03-16 = * Update - Show last 4 digit credit card number in order note when payment method is updated on failed renewal subscription order. * Update - Define constant for the group to be used for scheduled actions. * Update - Enable multiple customer currencies support in live mode. * Add - Rate limit failed account connection checks. * Add - Support displaying non-USD base fees on settings page. = 2.0.0 - 2021-02-22 = * Update - Render customer details in transactions list as text instead of link if order missing. * Update - Render transaction summary on details page for Multi-Currency transactions. * Update - Improvements to fraud prevention. * Fix - Added better notices for end users if there are connection errors when making payments. * Fix - If account is set to manual payouts display 'Temporarily suspended' under Payments > Settings. * Add - Add file dropzones to dispute evidence upload fields * Add - Currency conversion indicator to Transactions list. * Add - Transaction timeline details for Multi-Currency transactions. * Update - Link order note with transaction details page. * Fix - Updating payment method using saved payment for WC Subscriptions orders. = 1.9.2 - 2021-02-05 = * Fix - Checkout and cart blocks aren't usable in editor when WooCommerce Payments is enabled. * Fix - Missing global config error in Checkout block integration, and incompatibility with latest block API. = 1.9.1 - 2021-02-03 = * Fix - Incompatibility with WC Subscriptions. * Fix - Missing order causing broken transactions list. = 1.9.0 - 2021-02-02 = * Add - Improved fraud prevention. * Add - New setting to manage whether to enable saving cards during checkout. (Defaults to being enabled). * Fix - Fixed issue where an empty alert would appear when trying to refund an authorization charge. * Update - Link customer name on transaction detail page to filtered transaction list page. * Update - Test mode notice width is now consistent across all pages. * Fix - Fix error which could occur when a 100% off coupon was applied during checkout. * Add - New notification to urge setting SSL for checkout pages if store doesn't use HTTPS * Fix - Fixed connection timeout configuration. * Fix - Specify error code when refund fails in admin to prevent blank alert. * Fix - Add fees as line items sent to Stripe to prevent Level 3 errors. * Fix - Currency format in non-USD order note when capturing, refunding, and processing subscription renewal. * Update - Link customer name from transaction list page to WooCommerce's Customers page filtered by the customer's name. * Fix - Use proper currency information when rendering deposits overview and details. = 1.8.0 - 2020-12-16 = * Add - Include information about failing payment into order notes. * Fix - Fix crash when a user has 10 or more saved credit cards. * Fix - Fix crash if there's a problem connecting to the server. * Fix - Store Stripe customer for test and live mode. * Fix - Display Payments menu in the sidebar if there's a problem connecting to the server. * Add - Display fee structure in transaction timelines. * Add - Use site username for recording ToS acceptance. * Update - Display transaction tables with full width. * Add - Action hooks before and after webhook delivery. = 1.7.1 - 2020-12-03 = * Fix - Pass ISO strings instead of Moment objects to dateI18n. = 1.7.0 - 2020-11-17 = * Fix - Fix ordering of payment detail timeline events. * Fix - Payment form hides when saved card is selected. * Fix - Render dispute evidence file upload errors. * Fix - Increase timeout for calls to the API server. * Fix - Correctly display the fee and net amounts for a charge with an inquiry. * Fix - Catch unhandled exceptions when cancelling a payment authorization. * Add - Security.md with security and vulnerability reporting guidelines. * Add - Introduced "Set up refund policy" notification in WooCommerce inbox. * Fix - Fix error when retrying to save a card in the Add Payment Method screen after failing SCA authentication. * Add - Allow signing up for a subscription with free trial with a credit card that requires SCA authentication. * Add - Remote note service. * Add - Show details about the current fees in the Settings screen. = 1.6.0 - 2020-10-15 = * Fix - Trimming the whitespace when updating the bank statement descriptor. * Add - Initial support for the checkout block. * Add - Support wp_get_environment_type() and enable dev-mode when environment is 'development' or 'staging'. * Update - Introduced payments-specific exceptions instead of generic one. * Update - Transaction timeline: enabled timestamps rendering for all entries. = 1.5.0 - 2020-09-24 = * Fix - Save payment method checkbox for Subscriptions customer-initiated payment method updates. * Fix - Support checkout on Internet Explorer 11. * Fix - Webhook processing with no Jetpack plugin installed. * Fix - Do not block the checkout card field from loading when customer meta is invalid or related to an old account. * Fix - Saving account statement descriptor with an ampersand character. * Fix - Do not attempt to render the payment timeline if the intention ID is missing. * Add - Display payment method details on account subscriptions pages. * Add - Redact sensitive data before logging. * Add - Support for WooCommerce Subscriptions admin-initiated payment method changes. * Add - Link to Subscription admin pages from Transactions views. * Add - Support for Subscriptions in transaction search. = 1.4.1 - 2020-09-07 = * Fix - Only redirect to the onboarding screen if the plugin has been individually activated using the plugins page. = 1.4.0 - 2020-09-02 = * Add - Initial support for WooCommerce Subscriptions: Signing up for subscriptions, scheduled payments, and customer-initiated payment method changes. * Add - Added a link to transaction details from order screens. * Add - Allow merchant to edit statement descriptor. * Fix - Do not redirect to the onboarding page after completing the WC4.5-beta wizard. * Fix - Save order metadata before the payment is completed to avoid missing payments. * Update - Bumped the minimum Jetpack requirement to version 8.2. = 1.3.0 - 2020-08-17 = * Add - Support for saved cards. * Add - Search bar for transactions. * Fix - Redirect to WC core onboarding instead of WC Payments onboarding when appropriate. * Fix - Creating an account during checkout with 3DS cards. * Fix - Missing payment statuses for certain disputes. * Fix - Missing translators comments. = 1.2.0 - 2020-07-20 = * Add - 3DS support when using the pay for order page * Add - Display confirmation dialog when enabling manual captures * Add - Update the order when an authorised payment has expired * Add - Timeline view in transactions detail, requires WooCommerce 4.4 * Add - Tracking for basic user action events * Fix - Admin UI styling tweaks = 1.1.0 - 2020-06-16 = * Add - Allow WooCommerce Payments set up without Jetpack plugin * Fix - Orders missing after payment processing error * Fix - Clearing pagination when selecting transactions advanced filters * Fix - After onboarding, redirect to the WCPay task of the task list, instead of to the task list = 1.0.1 - 2020-06-01 = * Add - Support 3D Secure verification * Add - Transaction date and type filters to transactions list * Update - Redirect to the "Connect" page on plugin activation or when trying to navigate to the settings screen * Fix - Add deposit delay notice to deposit schedule * Fix - Avoid localizing deposit date or displaying time-of-day precision * Fix - Display dispute currency code in dispute info = 1.0.0 - 2020-05-19 = * Add - Level 3 data to payment requests * Update - Expose public method for checking connection status * Fix - Pending requirements state for improved messaging * Fix - Dispute details typo * Remove - Unused POST /charges endpoint * Remove - "Beta" messaging = 0.9.2 - 2020-05-14 = * Add - Customer ID to payment intent * Update - Register and enqueue js.stripe.com on WCPay admin pages * Update - Override needs_setup to redirect from Payments settings * Update - Copy and image on Connect Account screen * Add - Add deposits overview component * Add - URL to pass for prefilling OAuth form * Add - Test card details in Checkout * Add - Task list redirect upon return from OAuth flow * Add - Handling for failed refund and other webhooks * Add - Transaction list sorting * Update - Disable gateway when payments are disabled on the account * Update - Make table rows clickable * Add - Prompt before navigating away from unsaved dispute evidence changes * Update - Labels to sentence case * Update - Automated testing * Add - Metadata when creating payment intent * Update - PHP versions supported = 0.9.1 - 2020-04-09 = * Fix - Add logging for OAuth initialization failures = 0.9.0 - 2020-04-08 = * Add - Release for Public Beta. = 0.8.2 - 2020-03-10 = * Add - Dispute file evidence upload support * Add - Basic support for Pay for Order * Fix - Styling improvements in wp-admin * Fix - Undefined variable PHP notice in cancel_authorization * Fix - Remove unused variable in authentication * Fix - Improve Jetpack connection checking = 0.8.1 - 2020-02-25 = * Update - Link to test card documentation. = 0.8.0 - 2020-02-24 = * Add - First beta release. = 0.7.0 - 2020-02-05 = * Add - Alpha release. SECURITY.md 0000777 00000003627 15251156605 0006362 0 ustar 00 # Security Policy Full details of the Automattic Security Policy can be found on [automattic.com](https://automattic.com/security/). ## Supported Versions Generally, only the latest version of the extension has continued support. In some cases, we may opt to backport critical vulnerabilities fixes to previous versions. ## Reporting a Vulnerability [WooPayments](https://woocommerce.com/payments/) is an open-source plugin for WooCommerce. Our HackerOne program covers the plugin software. **For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.** _Please note that the **WordPress software is a separate entity** from Automattic. Please report vulnerabilities for WordPress through [the WordPress Foundation's HackerOne page](https://hackerone.com/wordpress)._ ## Guidelines We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines: * Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines). * Pen-testing Production: * Please **setup a local environment** instead whenever possible. Most of our code is open source (see above). * If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC. * **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels. * To be eligible for a bounty, all of these guidelines must be followed. * Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability. We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties. languages/woocommerce-payments.pot 0000777 00001566253 15251156605 0013452 0 ustar 00 # Copyright (C) 2026 WooCommerce # This file is distributed under the same license as the WooPayments package. msgid "" msgstr "" "Project-Id-Version: WooPayments 10.5.1\n" "Report-Msgid-Bugs-To: " "https://wordpress.org/support/plugin/woocommerce-payments\n" "POT-Creation-Date: 2026-02-10 17:15:18+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2026-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "X-Generator: node-wp-i18n 1.2.5\n" #: includes/admin/class-wc-payments-admin-settings.php:94 #. translators: %s: WooPayments msgid "%s is in test mode — all transactions are simulated!" msgstr "" #: includes/admin/class-wc-payments-admin-settings.php:102 #. translators: 1: Anchor opening tag; 2: Anchor closing tag msgid "" "You can use %1$stest card numbers%2$s to simulate various types of " "transactions." msgstr "" #: includes/admin/class-wc-payments-admin-settings.php:141 msgid "You are using a test account." msgstr "" #: includes/admin/class-wc-payments-admin-settings.php:146 #. translators: %s: URL to learn more msgid "" "Provide additional details about your business so you can begin accepting " "real payments. %1$sLearn more%2$s" msgstr "" #: includes/admin/class-wc-payments-admin-settings.php:151 #: includes/admin/class-wc-payments-admin-settings.php:232 msgid "" "⚠️ Development mode is enabled for the store! There can be no live " "onboarding process while using development, testing, or staging WordPress " "environments!" msgstr "" #: includes/admin/class-wc-payments-admin-settings.php:155 #: includes/admin/class-wc-payments-admin-settings.php:236 #. translators: 1: Anchor opening tag; 2: Anchor closing tag; 3: Anchor opening #. tag; 4: Anchor closing tag msgid "" "To begin accepting real payments, please go to the live store or change " "your %1$sWordPress environment%2$s to a production one. %3$sLearn more%4$s" msgstr "" #: includes/admin/class-wc-payments-admin-settings.php:167 #: dist/index.js:1 #: dist/settings.js:1 #: client/components/sandbox-mode-switch-to-live-notice/modal/index.tsx:59 #: client/overview/task-list/strings.tsx:89 msgid "Activate payments" msgstr "" #: includes/admin/class-wc-payments-admin-settings.php:220 msgid "You are using a sandbox test account." msgstr "" #: includes/admin/class-wc-payments-admin-settings.php:225 #. translators: 1: Anchor opening tag; 2: Anchor closing tag; 3: Anchor opening #. tag; 4: Anchor closing tag msgid "" "To begin accepting real payments you will need to first %1$sreset your " "account%2$s and, then, provide additional details about your business. " "%3$sLearn more%4$s" msgstr "" #: includes/admin/class-wc-payments-admin-settings.php:303 #: includes/admin/class-wc-payments-admin.php:505 #: includes/admin/class-wc-payments-admin.php:1177 #: dist/settings.js:1 #: client/settings/express-checkout-settings/index.js:149 msgid "Settings" msgstr "" #: includes/admin/class-wc-payments-admin.php:257 #: includes/admin/class-wc-payments-admin.php:297 msgid "Unsupported currency:" msgstr "" #: includes/admin/class-wc-payments-admin.php:263 #. translators: %s: WooPayments msgid "" "The selected currency is not available for the country set in your %s " "account." msgstr "" #: includes/admin/class-wc-payments-admin.php:304 #. Translators: %1$s: Opening anchor tag. %2$s: Closing anchor tag. msgid "" "Icelandic Króna does not accept decimals. Please update your currency " "number of decimals to 0 or select a different currency. %1$sVisit " "settings%2$s" msgstr "" #: includes/admin/class-wc-payments-admin.php:328 #: dist/index.js:1 #: client/index.js:90 msgid "Overview" msgstr "" #: includes/admin/class-wc-payments-admin.php:338 #: dist/index.js:1 #: dist/settings.js:1 #: client/components/deposits-overview/index.tsx:74 #: client/connect-account-page/strings.tsx:26 #: client/index.js:115 #: client/settings/settings-manager/index.js:92 msgid "Payouts" msgstr "" #: includes/admin/class-wc-payments-admin.php:348 #: dist/index.js:1 #: dist/settings.js:1 #: client/index.js:146 #: client/payment-details/readers/index.js:77 #: client/settings/settings-manager/index.js:74 #: client/transactions/index.tsx:58 #: client/transactions/list/index.tsx:391 msgid "Transactions" msgstr "" #: includes/admin/class-wc-payments-admin.php:358 #: dist/index.js:1 #: client/disputes/index.tsx:337 #: client/index.js:194 msgid "Disputes" msgstr "" #: includes/admin/class-wc-payments-admin.php:385 #: dist/index.js:1 #: client/index.js:284 msgid "Payments" msgstr "" #: includes/admin/class-wc-payments-admin.php:412 #: dist/index.js:1 #: client/index.js:64 msgid "Onboarding" msgstr "" #: includes/admin/class-wc-payments-admin.php:429 #: dist/index.js:1 #: client/index.js:78 msgid "Continue onboarding" msgstr "" #: includes/admin/class-wc-payments-admin.php:446 msgid "Card Readers" msgstr "" #: includes/admin/class-wc-payments-admin.php:459 #: dist/index.js:1 #: client/index.js:234 msgid "Capital Loans" msgstr "" #: includes/admin/class-wc-payments-admin.php:472 #: includes/class-wc-payments-status.php:575 #: dist/index.js:1 #: client/documents/list/index.tsx:130 #: client/index.js:248 msgid "Documents" msgstr "" #: includes/admin/class-wc-payments-admin.php:525 #: dist/index.js:1 #: client/deposits/details/index.tsx:90 #: client/index.js:117 msgid "Payout details" msgstr "" #: includes/admin/class-wc-payments-admin.php:533 #: dist/index.js:1 #: client/index.js:148 msgid "Payment details" msgstr "" #: includes/admin/class-wc-payments-admin.php:542 #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:593 #: client/index.js:177 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:144 msgid "Dispute details" msgstr "" #: includes/admin/class-wc-payments-admin.php:551 #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:720 #: client/index.js:196 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:137 msgid "Challenge dispute" msgstr "" #: includes/admin/class-wc-payments-admin.php:560 msgid "Add new payment methods" msgstr "" #: includes/admin/class-wc-payments-admin.php:568 #: dist/index.js:1 #: client/index.js:211 msgid "Set up multiple currencies" msgstr "" #: includes/admin/class-wc-payments-admin.php:821 msgid "" "Refunding manually requires reimbursing your customer offline via cash, " "check, etc. The refund amounts entered here will only be used to balance " "your analytics." msgstr "" #: includes/admin/class-wc-payments-admin.php:1223 #. translators: 1: WooPayments. msgid "Please <b>complete your %1$s setup</b> to continue using it." msgstr "" #: includes/admin/class-wc-payments-admin.php:1245 msgid "Paid with" msgstr "" #: includes/admin/class-wc-payments-admin.php:1249 #: includes/class-wc-payments-order-success-page.php:297 msgid "Card ending in" msgstr "" #: includes/admin/class-wc-payments-admin.php:1276 #. translators: %s: WooPayments msgid "This represents the fee %s collects for the transaction." msgstr "" #: includes/admin/class-wc-payments-admin.php:1281 msgid "Transaction Fee:" msgstr "" #: includes/admin/class-wc-rest-payments-charges-controller.php:79 #: includes/admin/class-wc-rest-payments-orders-controller.php:146 #: includes/admin/class-wc-rest-payments-orders-controller.php:286 #: includes/admin/class-wc-rest-payments-orders-controller.php:366 #: includes/admin/class-wc-rest-payments-orders-controller.php:422 #: includes/admin/class-wc-rest-payments-orders-controller.php:518 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:121 #: includes/admin/class-wc-rest-payments-reader-controller.php:295 msgid "Order not found" msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:155 msgid "ID for the payment method." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:160 msgid "Type of the payment method." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:166 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:255 msgid "Billing details for the payment method." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:171 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:260 msgid "Address associated with the billing details." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:176 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:265 msgid "City of the billing address." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:181 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:270 msgid "Country of the billing address." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:186 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:275 msgid "Line 1 of the billing address." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:191 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:280 msgid "Line 2 of the billing address." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:196 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:285 msgid "Postal code of the billing address." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:201 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:290 msgid "State of the billing address." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:208 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:297 msgid "Email associated with the billing details." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:214 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:303 msgid "Name associated with the billing details." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:219 #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:308 msgid "Phone number associated with the billing details." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:226 msgid "Card details for the payment method." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:231 msgid "Brand of the card." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:236 msgid "Last 4 digits of the card." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:241 msgid "Expiration month of the card." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:246 msgid "Expiration year of the card." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:253 msgid "SEPA Debit details for the payment method." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:258 msgid "Last 4 digits of the SEPA Debit." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:265 msgid "Link details for the payment method." msgstr "" #: includes/admin/class-wc-rest-payments-customer-controller.php:270 msgid "Email associated with the link." msgstr "" #: includes/admin/class-wc-rest-payments-documents-controller.php:94 #. translators: %1: The document ID. %2: The error message. msgid "There was an error accessing document %1$s. %2$s" msgstr "" #: includes/admin/class-wc-rest-payments-files-controller.php:103 msgid "Sorry, you are not allowed to do that." msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:153 #: includes/admin/class-wc-rest-payments-orders-controller.php:293 msgid "Payment cannot be captured for partially or fully refunded orders." msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:170 msgid "The payment cannot be captured for completed or processed orders." msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:183 #: includes/admin/class-wc-rest-payments-orders-controller.php:186 #: includes/admin/class-wc-rest-payments-orders-controller.php:308 #: includes/admin/class-wc-rest-payments-orders-controller.php:311 msgid "The payment cannot be captured" msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:191 #: includes/emails/class-wc-payments-email-ipp-receipt.php:106 msgid "WooCommerce In-Person Payments" msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:214 #: includes/admin/class-wc-rest-payments-orders-controller.php:325 #. translators: %s: the error message. msgid "Payment capture failed to complete with the following message: %s" msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:215 #: includes/admin/class-wc-rest-payments-orders-controller.php:326 #: includes/admin/class-wc-rest-payments-orders-controller.php:556 #: dist/subscription-edit-page.js:1 #: client/subscription-edit-page/index.tsx:130 msgid "Unknown error" msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:267 #: includes/admin/class-wc-rest-payments-orders-controller.php:346 #: includes/admin/class-wc-rest-payments-orders-controller.php:407 #: includes/admin/class-wc-rest-payments-orders-controller.php:449 #: includes/admin/class-wc-rest-payments-orders-controller.php:572 msgid "Unexpected server error" msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:379 msgid "Invalid order status" msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:525 msgid "Payment cannot be canceled for partially or fully refunded orders." msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:540 #: includes/admin/class-wc-rest-payments-orders-controller.php:543 msgid "The payment cannot be canceled" msgstr "" #: includes/admin/class-wc-rest-payments-orders-controller.php:555 #. translators: %s: the error message. msgid "Payment cancel failed to complete with the following message: %s" msgstr "" #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:166 msgid "ID for the payment intent." msgstr "" #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:171 #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:318 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:311 msgid "The amount of the transaction." msgstr "" #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:176 #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:313 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:306 msgid "The currency of the transaction." msgstr "" #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:181 msgid "The date when the payment intent was created." msgstr "" #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:186 msgid "The customer id of the intent" msgstr "" #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:191 msgid "The status of the payment intent." msgstr "" #: includes/admin/class-wc-rest-payments-payment-intents-controller.php:196 msgid "Charge object associated with this payment intention." msgstr "" #: includes/admin/class-wc-rest-payments-pm-promotions-controller.php:67 #: includes/admin/class-wc-rest-payments-pm-promotions-controller.php:85 msgid "The promotion unique identifier." msgstr "" #: includes/admin/class-wc-rest-payments-reader-controller.php:284 msgid "Invalid payment intent" msgstr "" #: includes/admin/class-wc-rest-payments-refunds-controller.php:61 msgid "Failed to create refund" msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:96 #. translators: %s: WooPayments msgid "If %s should be enabled." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:103 msgid "Payment method IDs that should be enabled. Other methods will be disabled." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:114 #. translators: %s: WooPayments msgid "If %s manual capture of charges should be enabled." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:123 #. translators: %s: WooPayments msgid "If %s \"Saved cards\" should be enabled." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:132 #. translators: %s: WooPayments msgid "%s test mode setting." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:141 #. translators: %s: WooPayments msgid "%s Multi-Currency feature flag setting." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:150 #. translators: %s: WooPayments msgid "%s Subscriptions feature flag setting." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:159 #. translators: %s: WooPayments msgid "%s bank account descriptor to be displayed in customers' bank accounts." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:166 msgid "The customer-facing business name." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:170 msgid "The business’s publicly available website." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:174 msgid "A publicly available mailing address for sending support issues to." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:179 msgid "A publicly available email address for sending support issues to." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:184 msgid "A publicly available phone number to call with support issues." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:189 msgid "A logo id for the account that will be used in Checkout" msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:193 msgid "An icon for the account." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:197 msgid "" "A CSS hex color value representing the primary branding color for this " "account." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:201 msgid "" "A CSS hex color value representing the secondary branding color for this " "account." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:205 msgid "Email address used for WooPayments communications." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:210 msgid "An interval for deposit scheduling." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:214 msgid "Weekly anchor for deposit scheduling when interval is set to weekly" msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:218 msgid "Monthly anchor for deposit scheduling when interval is set to monthly" msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:224 #. translators: %s: WooPayments msgid "If %s express checkouts should be enabled." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:231 msgid "" "If Apple Pay / Google Pay should be enabled as an option in the payment " "methods list." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:236 msgid "1-click checkout button types." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:245 msgid "1-click checkout button sizes." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:255 msgid "1-click checkout button themes." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:264 msgid "1-click checkout button border radius." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:269 msgid "If WooPay should be enabled." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:274 msgid "If Amazon Pay should be enabled." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:279 msgid "Custom message to display to WooPay customers." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:284 msgid "Store logo to display to WooPay customers." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:289 msgid "If Stripe Billing is enabled." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:294 msgid "" "Whether there is a Stripe Billing off-site to on-site billing migration in " "progress." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:299 msgid "The number of subscriptions using Stripe Billing" msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:304 msgid "The number of subscriptions migrated from Stripe Billing to on-site billing." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:309 msgid "Express checkout methods enabled on product page." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:318 msgid "Express checkout methods enabled on cart page." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:327 msgid "Express checkout methods enabled on checkout page." msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:394 #: includes/admin/class-wc-rest-payments-settings-controller.php:487 msgid "Error: Invalid email address: " msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:418 msgid "Error: Invalid phone number: " msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:427 msgid "Error: Invalid Japanese phone number: " msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:454 msgid "Error: Invalid address format!" msgstr "" #: includes/admin/class-wc-rest-payments-settings-controller.php:480 msgid "Error: Communications email is required." msgstr "" #: includes/admin/class-wc-rest-payments-settings-option-controller.php:108 #. translators: %s: expected type (bool, array, or string) msgid "Invalid value type; expected %s" msgstr "" #: includes/admin/class-wc-rest-payments-tos-controller.php:109 msgid "ToS accept parameter is missing" msgstr "" #: includes/admin/tasks/class-wc-payments-task-disputes.php:102 #. translators: %s is a currency formatted amount #: dist/index.js:1 #: client/overview/task-list/tasks/dispute-task.tsx:89 msgid "Respond to a dispute for %s – Last day" msgstr "" #: includes/admin/tasks/class-wc-payments-task-disputes.php:108 #. translators: %s is a currency formatted amount #: dist/index.js:1 #: client/overview/task-list/tasks/dispute-task.tsx:90 msgid "Respond to a dispute for %s" msgstr "" #: includes/admin/tasks/class-wc-payments-task-disputes.php:124 #. translators: %d is a number greater than 1. #: dist/index.js:1 #: client/overview/task-list/tasks/dispute-task.tsx:113 msgid "Respond to %d active disputes" msgstr "" #: includes/admin/tasks/class-wc-payments-task-disputes.php:145 #. translators: %d is a number greater than 1. %s is a formatted amount, eg: #. $10.00 msgid "Respond to %1$d active disputes for a total of %2$s" msgstr "" #: includes/admin/tasks/class-wc-payments-task-disputes.php:183 #. translators: %s is time, eg: 11:59 PM #: dist/index.js:1 #: client/overview/task-list/tasks/dispute-task.tsx:93 msgid "Respond today by %s" msgstr "" #: includes/admin/tasks/class-wc-payments-task-disputes.php:193 #. translators: %1$s is a date, eg: Jan 1, 2021. %2$s is the number of days #. left, eg: 2 days. msgid "By %1$s – %2$s left to respond" msgstr "" #: includes/admin/tasks/class-wc-payments-task-disputes.php:196 #: includes/class-wc-payments-order-success-page.php:111 #. translators: %s is the number of days left, e.g. 1 day. #. translators: %d: number of days msgid "%d day" msgid_plural "%d days" msgstr[0] "" msgstr[1] "" #: includes/admin/tasks/class-wc-payments-task-disputes.php:203 #. translators: %d is the number of disputes. #: dist/index.js:1 #: client/overview/task-list/tasks/dispute-task.tsx:124 msgid "Final day to respond to %d of the disputes" msgstr "" #: includes/admin/tasks/class-wc-payments-task-disputes.php:213 #. translators: %d is the number of disputes. #: dist/index.js:1 #: client/overview/task-list/tasks/dispute-task.tsx:125 msgid "Last week to respond to %d of the disputes" msgstr "" #: includes/class-duplicate-payment-prevention-service.php:132 #. translators: 1: charged amount, 2: current order total msgid "" "This order was already paid for %1$s, but the order total has since changed " "to %2$s, so we prevented an overpayment. Please create a new order for any " "additional items." msgstr "" #: includes/class-duplicate-payment-prevention-service.php:191 #. translators: order ID integer number msgid "" "WooCommerce Payments: detected and deleted order ID %d, which has duplicate " "cart content with this order." msgstr "" #: includes/class-payment-information.php:160 #: dist/blocks-checkout.js:1 #: dist/checkout.js:1 #: client/checkout/blocks/payment-processor.js:79 #: client/checkout/classic/payment-processing.js:445 msgid "" "Invalid or missing payment details. Please ensure the provided payment " "method is correctly entered." msgstr "" #: includes/class-payment-information.php:291 #: includes/class-wc-payment-gateway-wcpay.php:1697 #: includes/class-wc-payment-gateway-wcpay.php:2054 #: includes/class-wc-payment-gateway-wcpay.php:3634 #: includes/class-wc-payment-gateway-wcpay.php:3647 #: includes/class-wc-payment-gateway-wcpay.php:3659 msgid "We're not able to process this payment. Please try again later." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:410 #: dist/settings.js:1 #: client/settings/transactions/index.js:110 msgid "Customer bank statement" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:412 msgid "" "Edit the way your store name appears on your customers’ bank statements " "(read more about requirements <a>here</a>)." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:417 msgid "Manual capture" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:418 msgid "Issue an authorization on checkout, and capture later." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:420 msgid "" "Charge must be captured within 7 days of authorization, otherwise the " "authorization and order will be canceled." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:424 msgid "Saved cards" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:425 msgid "Enable payment via saved cards" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:427 msgid "" "If enabled, users will be able to pay with a saved card during checkout. " "Card details are saved on our platform, not on your store." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:432 #: dist/settings.js:1 #: client/settings/general-settings/index.js:55 msgid "Test mode" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:433 #: dist/settings.js:1 #: client/settings/general-settings/index.js:73 #: client/settings/general-settings/test-mode-confirm-modal.tsx:13 msgid "Enable test mode" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:435 msgid "Simulate transactions using test card numbers." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:440 msgid "Debug log" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:441 msgid "When enabled debug notes will be added to the log." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:447 msgid "Payment request buttons" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:452 msgid "Button type" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:454 msgid "Select the button type you would like to show." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:458 #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:75 msgid "Only icon" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:459 msgid "Buy" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:460 msgid "Donate" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:461 msgid "Book" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:465 msgid "Button theme" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:467 msgid "Select the button theme you would like to show." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:471 #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:94 msgid "Dark" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:472 #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:102 msgid "Light" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:473 msgid "Light-Outline" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:477 msgid "Button height" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:479 msgid "" "Enter the height you would like the button to be in pixels. Width will " "always be 100%." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:484 msgid "Custom button label" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:486 msgid "Enter the custom text you would like the button to have." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:487 msgid "Buy now" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:491 msgid "Button locations" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:493 msgid "Select where you would like to display the button." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:502 msgid "Product" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:503 msgid "Cart" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:504 msgid "Checkout" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:507 msgid "Select pages" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:511 #: dist/settings.js:1 #: client/settings/payment-methods-section/index.js:24 msgid "Payments accepted on checkout" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:517 msgid "Size of the button displayed for Express Checkouts" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:519 msgid "Select the size of the button." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:523 msgid "Small" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:524 msgid "Medium" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:525 msgid "Large" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:528 msgid "" "By placing this order, you agree to our [terms] and understand our " "[privacy_policy]." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:530 msgid "Express checkout methods on product page" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:534 #: includes/class-wc-payment-gateway-wcpay.php:545 #: includes/class-wc-payment-gateway-wcpay.php:556 #: dist/settings.js:1 #: client/settings/express-checkout-settings/express-checkout-settings-notices.tsx:50 #: client/settings/express-checkout/apple-google-pay-item.tsx:26 msgid "Apple Pay / Google Pay" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:535 #: includes/class-wc-payment-gateway-wcpay.php:546 #: includes/class-wc-payment-gateway-wcpay.php:557 #: includes/class-wc-payments-woopay-button-handler.php:334 #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: client/checkout/woopay/express-button/woopay-express-checkout-button.js:31 #: client/disable-confirmation-modal/index.js:145 #: client/payment-methods-icons.tsx:45 #: client/settings/express-checkout-settings/express-checkout-settings-notices.tsx:47 #: client/settings/express-checkout/woopay-item.tsx:38 msgid "WooPay" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:536 #: includes/class-wc-payment-gateway-wcpay.php:547 #: includes/class-wc-payment-gateway-wcpay.php:558 #: includes/payment-methods/Configs/Definitions/AmazonPayDefinition.php:56 #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: client/constants/payment-method.ts:50 #: client/settings/express-checkout-settings/express-checkout-settings-notices.tsx:53 msgid "Amazon Pay" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:537 #: includes/class-wc-payment-gateway-wcpay.php:548 #: includes/class-wc-payment-gateway-wcpay.php:559 #: includes/payment-methods/Configs/Definitions/LinkDefinition.php:55 msgid "Link" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:541 msgid "Express checkout methods on cart page" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:552 msgid "Express checkout methods on checkout page" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:695 msgid "" "We're not able to process this payment due to the order ID mismatch. Please " "try again later." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:811 msgid "" "Next we’ll ask you to share a few details about your business to create " "your account." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1068 #: includes/class-wc-payment-gateway-wcpay.php:1073 msgid "Return to payments" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1102 msgid "Save payment information to my account for future purchases." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1148 msgid "Invalid phone number." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1158 #: includes/class-wc-payment-gateway-wcpay.php:3625 msgid "" "We're not able to process this payment. Please refresh the page and try " "again." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1166 msgid "Your payment was not processed." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1229 #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:394 #. translators: %1: the failed payment amount, %2: error message msgid "" "A payment of %1$s <strong>failed</strong> to complete with the following " "message: <code>%2$s</code>." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1246 #. translators: %1: the failed payment amount, %2: error message msgid "A payment of %1$s <strong>failed</strong>. %2$s" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1251 msgid "" "We couldn’t verify the postal code in the billing address. If the issue " "persists, suggest the customer to reach out to the card issuing bank." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1280 #. translators: %1: the failed payment amount msgid "" "A payment of %1$s <strong>failed</strong> to complete because of too many " "failed transactions. A rate limiter was enabled for the user to prevent " "more attempts temporarily." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1527 #. translators: %1: the last 4 digit of the credit card msgid "Payment method is changed to: <strong>Credit card ending in %1$s</strong>." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1539 #: includes/payment-methods/class-cc-payment-method.php:104 msgid "Credit / Debit Cards" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1577 #. translators: %s: metadata. We do not need to translate WooPayMeta msgid "" "We're not able to process this payment. Please try again later. WooPayMeta: " "intent_meta_order_id: %1$s, order_id: %2$s" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:1928 msgid "Payment method successfully added." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:2116 #. translators: localized exception message msgid "UPE payment failed: %s" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:2395 #. translators: an error message which will appear if a user tries to refund an #. order which is has been authorized but not yet charged. msgid "" "This payment is not captured yet. To cancel this order, please go to 'Order " "Actions' > 'Cancel authorization'. To proceed with a refund, please go to " "'Order Actions' > 'Capture charge' to charge the payment card, and then " "trigger a refund via the 'Refund' button." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:2409 msgid "The refund amount is not valid." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:2437 msgid "" "You shall refund this payment in the same application where the payment was " "made." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:2463 #. translators: %1: the successfully charged amount, %2: error message msgid "A refund of %1$s failed to complete: %2$s" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:2480 #. translators: %1$: order id. msgid "A refund cannot be found for order: %1$s" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3340 msgid "Failed to update Stripe account. " msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3370 msgid "" "Customer bank statement is invalid. Statement should be between 5 and 22 " "characters long, contain at least single Latin character and does not " "contain special characters: ' \" * < >" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3402 msgid "Capture charge" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3403 msgid "Cancel authorization" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3459 #. translators: %s: formatted minimum amount with currency msgid "The minimum amount to capture is %s." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3562 #. translators: %1: error message msgid "" "Canceling authorization <strong>failed</strong> to complete with the " "following message: <code>%1$s</code>." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3577 #: includes/class-wc-payments-order-service.php:1388 msgid "Canceling authorization <strong>failed</strong> to complete." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3643 #: includes/payment-methods/class-cc-payment-method.php:50 #. translators: This will be used to indicate an unknown value for an ID. #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:42 #: client/payment-details/payment-method/card/index.js:49 msgid "unknown" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3724 msgid "Payment method updated." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3727 msgid "Payment method updated for all your current subscriptions." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3752 #. translators: %1: transaction ID of the payment or a translated string #. indicating an unknown ID. msgid "" "A payment with ID <code>%1$s</code> was used in an attempt to pay for this " "order. This payment intent ID does not match any payments for this order, " "so it was ignored and the order was not updated." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3798 #. translators: %s: WooPayments msgid "A %s payment method was not provided" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3812 msgid "We're not able to add this payment method. Please try again later" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3823 msgid "Failed to add the provided payment method. Please try again later" msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3932 msgid "" "We're not able to add this payment method. Please refresh the page and try " "again." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:3939 msgid "" "You cannot add a new payment method so soon after the previous one. Please " "try again later." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:4228 #: includes/class-wc-payment-gateway-wcpay.php:4238 msgid "Unable to update UPE appearance values at this time." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:4517 #. translators: %1$s: WooPayments msgid "" "%1$s gives your store flexibility to accept credit cards, debit cards, and " "Apple Pay. Enable popular local payment methods and other digital wallets " "like Google Pay to give customers even more choice." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:4527 #. translators: %s: WooPay, msgid "Payments made simple — including %s, a new express checkout feature." msgstr "" #: includes/class-wc-payment-gateway-wcpay.php:4615 msgid "A valid shipping address is required for Afterpay payments." msgstr "" #: includes/class-wc-payment-token-wcpay-link.php:53 #. translators: customer email msgid "Stripe Link email ending in %s" msgstr "" #: includes/class-wc-payment-token-wcpay-sepa.php:53 #. translators: last 4 digits of IBAN account msgid "SEPA IBAN ending in %s" msgstr "" #: includes/class-wc-payments-account.php:247 msgid "Failed to detect connection status" msgstr "" #: includes/class-wc-payments-account.php:1482 #. translators: %s: WooPayments msgid "" "Connection to WordPress.com failed. Please connect to WordPress.com to " "start using %s." msgstr "" #: includes/class-wc-payments-account.php:1547 #. translators: 1: WooPayments. msgid "Please <b>complete your %1$s setup</b> to process transactions." msgstr "" #: includes/class-wc-payments-account.php:1597 #. translators: %s: error message. msgid "There was a problem connecting your store to WordPress.com: \"%s\"" msgstr "" #: includes/class-wc-payments-account.php:1640 msgid "" "There was a duplicate attempt to initiate account setup. Please wait a few " "seconds and try again." msgstr "" #: includes/class-wc-payments-account.php:1696 #. translators: 1: anchor opening markup 2: closing anchor markup msgid "" "Another account setup session is already in progress. Please finish it or " "%1$sclick here to start again%2$s." msgstr "" #: includes/class-wc-payments-account.php:1746 #. translators: %s: WooPayments. msgid "There was a problem setting up your %s account. Please try again." msgstr "" #: includes/class-wc-payments-account.php:2218 msgid "There was a problem processing your account data. Please try again." msgstr "" #: includes/class-wc-payments-account.php:2504 msgid "Failed to update Account locale. " msgstr "" #: includes/class-wc-payments-address-provider.php:59 msgid "WooCommerce Payments" msgstr "" #: includes/class-wc-payments-apple-pay-registration.php:174 msgid "Your domain has been verified with Apple Pay!" msgstr "" #: includes/class-wc-payments-apple-pay-registration.php:293 msgid "Apple Pay domain verification failed." msgstr "" #: includes/class-wc-payments-apple-pay-registration.php:294 msgid "Apple Pay domain verification failed with the following error:" msgstr "" #: includes/class-wc-payments-apple-pay-registration.php:297 #. translators: a: Link to the logs page msgid "" "Please check the <a>logs</a> for more details on this issue. Debug log must " "be enabled under <strong>Advanced settings</strong> to see recorded logs." msgstr "" #: includes/class-wc-payments-apple-pay-registration.php:304 msgid "<a>Learn more</a>." msgstr "" #: includes/class-wc-payments-apple-pay-registration.php:313 msgid "Express checkouts:" msgstr "" #: includes/class-wc-payments-captured-event-note.php:125 #: dist/index.js:1 #: dist/settings.js:1 #: client/payment-details/timeline/map-events.js:356 #: client/payment-details/transaction-breakdown/utils.ts:12 #: client/utils/account-fees.tsx:99 msgid "Base fee" msgstr "" #: includes/class-wc-payments-captured-event-note.php:126 #: dist/index.js:1 #: client/payment-details/readers/index.js:83 #: client/payment-details/timeline/map-events.js:357 #: client/payment-details/transaction-breakdown/utils.ts:35 msgid "Fee" msgstr "" #: includes/class-wc-payments-captured-event-note.php:212 #. translators: %s is a monetary amount #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:305 #. %s is a monetary amount msgid "Net payout: %s" msgstr "" #: includes/class-wc-payments-captured-event-note.php:271 #. translators: %s is a percentage number #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:611 #. %s is a percentage number msgid "Variable fee: %s" msgstr "" #: includes/class-wc-payments-captured-event-note.php:276 #. translators: %s is a monetary amount #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:616 #. %s is a monetary amount msgid "Fixed fee: %s" msgstr "" #: includes/class-wc-payments-captured-event-note.php:313 #. translators: 1: tax description 2: tax percentage 3: tax amount #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:332 #. 1: tax description 2: tax percentage 3: tax amount msgid "Tax%1$s%2$s: %3$s" msgstr "" #: includes/class-wc-payments-captured-event-note.php:364 #. translators: %2$s is the capped fee #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:507 #. %2$s is the capped fee msgid "Base fee: capped at %2$s" msgstr "" #: includes/class-wc-payments-captured-event-note.php:368 #. translators: %1$s% is the fee percentage and %2$s is the fixed rate #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:512 #. %1$s% is the fee percentage and %2$s is the fixed rate msgid "Base fee: %1$s%% + %2$s" msgstr "" #: includes/class-wc-payments-captured-event-note.php:370 #. translators: %1$s% is the fee percentage #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:516 #. %1$s% is the fee percentage msgid "Base fee: %1$s%%" msgstr "" #: includes/class-wc-payments-captured-event-note.php:375 #. translators: %1$s% is the fee percentage and %2$s is the fixed rate #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:521 msgid "International card fee: %1$s%% + %2$s" msgstr "" #: includes/class-wc-payments-captured-event-note.php:377 #. translators: %1$s% is the fee percentage #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:526 msgid "International card fee: %1$s%%" msgstr "" #: includes/class-wc-payments-captured-event-note.php:381 #. translators: %1$s% is the fee percentage and %2$s is the fixed rate #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:533 msgid "Currency conversion fee: %1$s%% + %2$s" msgstr "" #: includes/class-wc-payments-captured-event-note.php:383 #. translators: %1$s% is the fee percentage #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:538 msgid "Currency conversion fee: %1$s%%" msgstr "" #: includes/class-wc-payments-captured-event-note.php:387 #. translators: %1$s% is the fee percentage and %2$s is the fixed rate #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:545 msgid "Subscription transaction fee: %1$s%% + %2$s" msgstr "" #: includes/class-wc-payments-captured-event-note.php:389 #. translators: %1$s% is the fee percentage #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:550 msgid "Subscription transaction fee: %1$s%%" msgstr "" #: includes/class-wc-payments-captured-event-note.php:391 #: includes/express-checkout/class-wc-payments-express-checkout-button-helper.php:129 #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:160 #: dist/express-checkout.js:1 #: dist/blocks-checkout.js:1 #: dist/index.js:1 #: client/express-checkout/transformers/wc-to-stripe.js:125 #: client/payment-details/timeline/map-events.js:567 msgid "Discount" msgstr "" #: includes/class-wc-payments-captured-event-note.php:532 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:15 msgid "AT VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:533 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:16 msgid "BE VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:534 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:17 msgid "BG VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:535 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:18 msgid "CY VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:536 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:19 msgid "CZ VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:537 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:20 msgid "DE VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:538 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:21 msgid "DK VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:539 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:22 msgid "EE VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:540 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:23 msgid "ES VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:541 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:24 msgid "FI VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:542 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:25 msgid "FR VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:543 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:26 msgid "UK VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:544 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:27 msgid "GR VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:545 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:28 msgid "HR VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:546 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:29 msgid "HU VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:547 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:30 msgid "IE VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:548 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:31 msgid "IT VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:549 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:32 msgid "LT VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:550 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:33 msgid "LU VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:551 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:34 msgid "LV VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:552 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:35 msgid "MT VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:553 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:36 msgid "NO VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:554 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:37 msgid "NL VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:555 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:38 msgid "PL VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:556 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:39 msgid "PT VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:557 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:40 msgid "RO VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:558 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:41 msgid "SE VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:559 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:42 msgid "SI VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:560 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:43 msgid "SK VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:563 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:45 msgid "AU GST" msgstr "" #: includes/class-wc-payments-captured-event-note.php:564 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:46 msgid "NZ GST" msgstr "" #: includes/class-wc-payments-captured-event-note.php:565 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:47 msgid "SG GST" msgstr "" #: includes/class-wc-payments-captured-event-note.php:568 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:49 msgid "CH VAT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:569 #: dist/index.js:1 #: client/payment-details/utils/tax-descriptions.ts:50 msgid "JP JCT" msgstr "" #: includes/class-wc-payments-captured-event-note.php:572 #: includes/express-checkout/class-wc-payments-express-checkout-button-helper.php:112 #: includes/express-checkout/class-wc-payments-express-checkout-button-helper.php:701 #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:185 #: dist/express-checkout.js:1 #: dist/blocks-checkout.js:1 #: dist/index.js:1 #: client/express-checkout/transformers/wc-to-stripe.js:150 #: client/payment-details/utils/tax-descriptions.ts:52 msgid "Tax" msgstr "" #: includes/class-wc-payments-checkout.php:194 msgid "" "There was a problem processing the payment. Please check your email inbox " "and refresh the page to try again." msgstr "" #: includes/class-wc-payments-checkout.php:348 #: includes/class-wc-payments-checkout.php:457 #. translators: link to Stripe testing page msgid "Click to copy the test number to clipboard" msgstr "" #: includes/class-wc-payments-checkout.php:348 #: includes/class-wc-payments-checkout.php:457 #: dist/index.js:1 #: client/components/copy-button/index.tsx:19 msgid "Copy to clipboard" msgstr "" #: includes/class-wc-payments-checkout.php:497 #: dist/checkout.js:1 #: dist/blocks-checkout.js:1 #: client/checkout/utils/fingerprint.js:12 msgid "" "An error was encountered when preparing the payment form. Please try again " "later." msgstr "" #: includes/class-wc-payments-customer-service.php:309 #. translators: %1$s Name, %2$s Username. msgid "Name: %1$s, Username: %2$s" msgstr "" #: includes/class-wc-payments-customer-service.php:313 #. translators: %1$s Name. msgid "Name: %1$s, Guest" msgstr "" #: includes/class-wc-payments-dependency-service.php:204 #. translators: %1$s: WooPayments, %2$s: WooCommerce msgid "%1$s requires <a>%2$s</a> to be installed and active." msgstr "" #: includes/class-wc-payments-dependency-service.php:215 msgid "Install WooCommerce" msgstr "" #: includes/class-wc-payments-dependency-service.php:219 msgid "Activate WooCommerce" msgstr "" #: includes/class-wc-payments-dependency-service.php:229 #. translators: %1: WooPayments, %2: current WooCommerce Payment version, %3: #. WooCommerce, %4: required WC version number, %5: currently installed WC #. version number msgid "" "%1$s %2$s requires <strong>%3$s %4$s</strong> or greater to be installed " "(you are using %5$s). " msgstr "" #: includes/class-wc-payments-dependency-service.php:245 #. translators: %1$s: WooCommerce, %2$s: WooPayments, a1: link to the Plugins #. page, a2: link to the page having all previous versions msgid "" "<a1>Update %1$s</a1> <strong>(recommended)</strong> or manually re-install " "<a2>a previous version</a2> of %2$s." msgstr "" #: includes/class-wc-payments-dependency-service.php:263 #. translators: %1$s: WooPayments, %2$s: WooCommerce Admin msgid "" "%1$s requires %2$s to be enabled. Please remove the " "<code>woocommerce_admin_disabled</code> filter to use %1$s." msgstr "" #: includes/class-wc-payments-dependency-service.php:275 #. translators: %1: WooPayments, %2: WooCommerce Admin, %3: required WC-Admin #. version number, %4: currently installed WC-Admin version number msgid "" "%1$s requires <strong>%2$s %3$s</strong> or greater to be installed (you " "are using %4$s)." msgstr "" #: includes/class-wc-payments-dependency-service.php:285 msgid "There is a newer version of WooCommerce Admin bundled with WooCommerce." msgstr "" #: includes/class-wc-payments-dependency-service.php:289 msgid "Use the bundled version of WooCommerce Admin" msgstr "" #: includes/class-wc-payments-dependency-service.php:297 #. translators: %1: WooPayments, %2: required WP version number, %3: currently #. installed WP version number msgid "" "%1$s requires <strong>WordPress %2$s</strong> or greater (you are using " "%3$s)." msgstr "" #: includes/class-wc-payments-dependency-service.php:305 msgid "Update WordPress" msgstr "" #: includes/class-wc-payments-dependency-service.php:312 #. translators: %s: WooPayments msgid "" "You have installed a development version of %s which requires files to be " "built. From the plugin directory, run <code>npm run build:client</code> to " "build and minify assets. Alternatively, you can download a pre-built " "version of the plugin from the <a1>WordPress.org repository</a1> or by " "visiting the <a2>releases page in the GitHub repository</a2>." msgstr "" #: includes/class-wc-payments-onboarding-service.php:318 #: includes/class-wc-payments-onboarding-service.php:694 msgid "" "Onboarding initialization is already in progress. Please wait for it to " "finish." msgstr "" #: includes/class-wc-payments-onboarding-service.php:425 msgid "Failed to finalize onboarding session." msgstr "" #: includes/class-wc-payments-onboarding-service.php:689 #: includes/class-wc-payments-onboarding-service.php:791 #: includes/class-wc-payments-onboarding-service.php:846 msgid "Your store is not connected to WordPress.com. Please connect it first." msgstr "" #: includes/class-wc-payments-onboarding-service.php:796 msgid "Failed to reset the account: account does not exist." msgstr "" #: includes/class-wc-payments-onboarding-service.php:807 msgid "Failed to delete account." msgstr "" #: includes/class-wc-payments-onboarding-service.php:851 msgid "Failed to activate the account: account does not exist." msgstr "" #: includes/class-wc-payments-onboarding-service.php:875 msgid "Failed to disable test drive account." msgstr "" #: includes/class-wc-payments-order-service.php:427 msgid "Dispute lost." msgstr "" #: includes/class-wc-payments-order-service.php:562 msgid "<strong>Fee details:</strong>" msgstr "" #: includes/class-wc-payments-order-service.php:1441 msgid "Capture authorization <strong>failed</strong> to complete." msgstr "" #: includes/class-wc-payments-order-service.php:1541 #. translators: %1$s: the refund amount, %2$s: status (cancelled/unsuccessful), #. %3$s: WooPayments, %4$s: ID of the refund, %5$s: failure message or period msgid "" "A refund of %1$s was <strong>%2$s</strong> using %3$s " "(<code>%4$s</code>)%5$s" msgstr "" #: includes/class-wc-payments-order-service.php:1548 #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:16 msgid "cancelled" msgstr "" #: includes/class-wc-payments-order-service.php:1548 msgid "unsuccessful" msgstr "" #: includes/class-wc-payments-order-service.php:1585 #. translators: %1: the successfully charged amount, %2: WooPayments, %3: #. transaction ID of the payment msgid "" "A payment of %1$s was <strong>successfully charged</strong> using %2$s " "(<a>%3$s</a>)." msgstr "" #: includes/class-wc-payments-order-service.php:1612 #. translators: %1: the authorized amount, %2: WooPayments, %3: transaction ID #. of the payment msgid "A payment of %1$s <strong>failed</strong> using %2$s (<a>%3$s</a>)." msgstr "" #: includes/class-wc-payments-order-service.php:1646 #. translators: %1: the authorized amount, %2: WooPayments, %3: transaction ID #. of the payment, %4: timestamp msgid "A terminal payment of %1$s <strong>failed</strong> using %2$s (<a>%3$s</a>)" msgstr "" #: includes/class-wc-payments-order-service.php:1678 #. translators: %1: the authorized amount, %2: WooPayments, %3: transaction ID #. of the payment msgid "A payment of %1$s was <strong>authorized</strong> using %2$s (<a>%3$s</a>)." msgstr "" #: includes/class-wc-payments-order-service.php:1704 #. translators: %1: the authorized amount, %2: WooPayments, %3: intent ID of #. the payment msgid "" "A payment of %1$s was <strong>started</strong> using %2$s " "(<code>%3$s</code>)." msgstr "" #: includes/class-wc-payments-order-service.php:1732 #. translators: %1: the successfully charged amount, %2: WooPayments, %3: #. transaction ID of the payment msgid "" "A payment of %1$s was <strong>successfully captured</strong> using %2$s " "(<a>%3$s</a>)." msgstr "" #: includes/class-wc-payments-order-service.php:1760 #. translators: %1: the authorized amount, %2: WooPayments, %3: transaction ID #. of the payment msgid "" "A capture of %1$s <strong>failed</strong> to complete using %2$s " "(<a>%3$s</a>)." msgstr "" #: includes/class-wc-payments-order-service.php:1792 #. translators: %1: the authorized amount, %2: transaction ID of the payment msgid "Payment authorization has <strong>expired</strong> (<a>%1$s</a>)." msgstr "" #: includes/class-wc-payments-order-service.php:1816 #. translators: %1: transaction ID of the payment msgid "" "Payment authorization was successfully <strong>cancelled</strong> " "(<a>%1$s</a>)." msgstr "" #: includes/class-wc-payments-order-service.php:1848 #. translators: %1: the authorized amount, %2: transaction ID of the payment msgid "" "⛔ A payment of %1$s was <strong>held for review</strong> by one or " "more risk filters.<br><br><a>View more details</a>." msgstr "" #: includes/class-wc-payments-order-service.php:1882 #. translators: %1: the blocked amount, %2: transaction ID of the payment msgid "" "🚫 A payment of %1$s was <strong>blocked</strong> by one or more " "risk filters.<br><br><a>View more details</a>." msgstr "" #: includes/class-wc-payments-order-service.php:1917 #. translators: %1: the disputed amount and currency; %2: the dispute reason; #. %3 the deadline date for responding to the inquiry msgid "" "A payment inquiry has been raised for %1$s with reason \"%2$s\". " "<a>Response due by %3$s</a>." msgstr "" #: includes/class-wc-payments-order-service.php:1932 #. translators: %1: the disputed amount and currency; %2: the dispute reason; #. %3 the deadline date for responding to dispute msgid "" "Payment has been disputed for %1$s with reason \"%2$s\". <a>Response due by " "%3$s</a>." msgstr "" #: includes/class-wc-payments-order-service.php:1960 #. translators: %1: the dispute status msgid "" "Payment inquiry has been closed with status %1$s. See <a>payment status</a> " "for more details." msgstr "" #: includes/class-wc-payments-order-service.php:1973 #. translators: %1: the dispute status msgid "" "Dispute has been closed with status %1$s. See <a>dispute overview</a> for " "more details." msgstr "" #: includes/class-wc-payments-order-service.php:2002 msgid "is pending" msgstr "" #: includes/class-wc-payments-order-service.php:2004 msgid "was successfully processed" msgstr "" #: includes/class-wc-payments-order-service.php:2010 #. translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund, #. %4: status text msgid "A refund of %1$s %4$s using %2$s (<code>%3$s</code>)." msgstr "" #: includes/class-wc-payments-order-service.php:2024 #. translators: %1: the refund amount, %2: WooPayments, %3: reason, %4: refund #. id, %5: status text msgid "A refund of %1$s %5$s using %2$s. Reason: %3$s. (<code>%4$s</code>)" msgstr "" #: includes/class-wc-payments-order-service.php:2207 msgid "Intent id was not included for payment complete status change." msgstr "" #: includes/class-wc-payments-order-service.php:2280 msgid "The requested order was not found." msgstr "" #: includes/class-wc-payments-order-service.php:2423 #. translators: %s: Formatted refund amount msgid "" "Refund of %s <strong>failed</strong> due to insufficient funds in your " "WooPayments balance. To prevent delays in refunding customers, please " "consider adding funds to your Future Refunds or Disputes (FROD) balance. " "<a>Learn more</a>." msgstr "" #: includes/class-wc-payments-order-service.php:2443 #. translators: %1$s: Formatted refund amount msgid "" "Refund of %1$s <strong>failed</strong> due to insufficient funds in your " "WooPayments balance." msgstr "" #: includes/class-wc-payments-order-success-page.php:76 msgid "" "Your order is on hold until payment is received. Please follow the payment " "instructions by the expiry date." msgstr "" #: includes/class-wc-payments-order-success-page.php:84 #: includes/class-wc-payments-order-success-page.php:584 #: includes/payment-methods/Configs/Definitions/MultibancoDefinition.php:56 msgid "Multibanco" msgstr "" #: includes/class-wc-payments-order-success-page.php:90 #: includes/class-wc-payments-order-success-page.php:590 #. translators: %s: order number msgid "Order #%s" msgstr "" #: includes/class-wc-payments-order-success-page.php:98 #: includes/class-wc-payments-order-success-page.php:600 #. translators: %s: expiry date msgid "Expires <strong>%s</strong>" msgstr "" #: includes/class-wc-payments-order-success-page.php:122 #: includes/class-wc-payments-order-success-page.php:613 msgid "Payment instructions" msgstr "" #: includes/class-wc-payments-order-success-page.php:124 #: includes/class-wc-payments-order-success-page.php:564 #: includes/class-wc-payments-order-success-page.php:615 msgid "" "In your online bank account or from an ATM, choose \"Payment and other " "services\"." msgstr "" #: includes/class-wc-payments-order-success-page.php:125 #: includes/class-wc-payments-order-success-page.php:565 #: includes/class-wc-payments-order-success-page.php:616 msgid "Click \"Payments of services/shopping\"." msgstr "" #: includes/class-wc-payments-order-success-page.php:126 #: includes/class-wc-payments-order-success-page.php:566 #: includes/class-wc-payments-order-success-page.php:617 msgid "Enter the entity number, reference number, and amount." msgstr "" #: includes/class-wc-payments-order-success-page.php:132 #: includes/class-wc-payments-order-success-page.php:567 #: includes/class-wc-payments-order-success-page.php:623 msgid "Entity" msgstr "" #: includes/class-wc-payments-order-success-page.php:136 #: includes/class-wc-payments-order-success-page.php:568 #: includes/class-wc-payments-order-success-page.php:627 #: dist/index.js:1 #: client/payment-details/payment-method/p24/index.js:88 msgid "Reference" msgstr "" #: includes/class-wc-payments-order-success-page.php:140 #: includes/class-wc-payments-order-success-page.php:569 #: includes/class-wc-payments-order-success-page.php:631 #: dist/index.js:1 #: client/capital/index.tsx:42 #: client/components/deposits-overview/recent-deposits-list.tsx:39 #: client/deposits/list/index.tsx:65 #: client/disputes/index.tsx:74 #: client/transactions/blocked/columns.tsx:32 #: client/transactions/list/index.tsx:137 #: client/transactions/uncaptured/index.tsx:59 msgid "Amount" msgstr "" #: includes/class-wc-payments-order-success-page.php:145 msgid "Print" msgstr "" #: includes/class-wc-payments-order-success-page.php:146 msgid "Copy link for sharing" msgstr "" #: includes/class-wc-payments-order-success-page.php:242 #: includes/class-wc-payments-order-success-page.php:273 msgid "•••" msgstr "" #: includes/class-wc-payments-order-success-page.php:354 msgid "" "We detected and prevented an attempt to pay for a duplicate order. If this " "was a mistake and you wish to try again, please create a new order." msgstr "" #: includes/class-wc-payments-order-success-page.php:371 msgid "" "We prevented multiple payments for the same order. If this was a mistake " "and you wish to try again, please create a new order." msgstr "" #: includes/class-wc-payments-order-success-page.php:463 #. translators: %s: checkout URL msgid "" "Unfortunately, your order has failed. Please <a href=\"%s\">try checking " "out again</a>." msgstr "" #: includes/class-wc-payments-order-success-page.php:558 msgid "Multibanco Payment instructions" msgstr "" #: includes/class-wc-payments-order-success-page.php:561 #. translators: %s: expiry date msgid "Expires %s" msgstr "" #: includes/class-wc-payments-payment-request-session-handler.php:65 msgid "Invalid token: cookie and session customer mismatch" msgstr "" #: includes/class-wc-payments-pm-promotions-service.php:854 #. translators: %s is the payment method title, e.g., "Klarna" #: dist/settings.js:1 #: client/settings/general-settings/enable-woopayments-checkbox.js:52 #. %s: WooPayments msgid "Enable %s" msgstr "" #: includes/class-wc-payments-pm-promotions-service.php:865 msgid "See terms" msgstr "" #: includes/class-wc-payments-status.php:92 #. translators: %s: WooPayments msgid "Clear %s account cache" msgstr "" #: includes/class-wc-payments-status.php:95 msgid "Clear" msgstr "" #: includes/class-wc-payments-status.php:98 #. translators: %s: WooPayments msgid "This tool will clear the cached account values used in %s." msgstr "" #: includes/class-wc-payments-status.php:106 #. translators: %s: WooPayments msgid "Delete %s test orders" msgstr "" #: includes/class-wc-payments-status.php:109 msgid "Delete" msgstr "" #: includes/class-wc-payments-status.php:112 #. translators: %s: WooPayments msgid "" "<strong class=\"red\">Note:</strong> This option deletes all test mode " "orders placed via %s. Orders placed via other gateways will not be " "affected. Use with caution, as this action cannot be undone." msgstr "" #: includes/class-wc-payments-status.php:118 msgid "Preview canceled authorization fix (Dry Run)" msgstr "" #: includes/class-wc-payments-status.php:120 msgid "" "Preview what orders would be affected by the canceled authorization fix " "without making any changes. Results are logged to WooCommerce > Status > " "Logs." msgstr "" #: includes/class-wc-payments-status.php:125 msgid "Fix canceled authorization analytics" msgstr "" #: includes/class-wc-payments-status.php:128 msgid "" "This will update order metadata and delete incorrect refund records for " "affected orders. This fixes negative values in WooCommerce Analytics. Make " "sure you have a recent backup before proceeding. Continue?" msgstr "" #: includes/class-wc-payments-status.php:146 msgid "You do not have permission to delete orders." msgstr "" #: includes/class-wc-payments-status.php:163 msgid "No test orders found." msgstr "" #: includes/class-wc-payments-status.php:176 #. translators: %d: number of orders deleted msgid "%d test order has been permanently deleted." msgid_plural "%d test orders have been permanently deleted." msgstr[0] "" msgstr[1] "" #: includes/class-wc-payments-status.php:187 #. translators: %s: error message msgid "Error deleting test orders: %s" msgstr "" #: includes/class-wc-payments-status.php:204 #: includes/class-wc-payments-status.php:248 msgid "You do not have permission to run this tool." msgstr "" #: includes/class-wc-payments-status.php:213 #: includes/class-wc-payments-status.php:257 msgid "Remediation has already been completed." msgstr "" #: includes/class-wc-payments-status.php:220 #: includes/class-wc-payments-status.php:264 msgid "Remediation is already in progress. Check the Action Scheduler for status." msgstr "" #: includes/class-wc-payments-status.php:227 msgid "" "Remediation has been scheduled and will run in the background. You can " "monitor progress in the Action Scheduler." msgstr "" #: includes/class-wc-payments-status.php:232 #. translators: %s: error message msgid "Error scheduling remediation: %s" msgstr "" #: includes/class-wc-payments-status.php:271 msgid "" "Dry run has been scheduled and will run in the background. Check " "WooCommerce > Status > Logs for results (source: wcpay-fee-remediation)." msgstr "" #: includes/class-wc-payments-status.php:276 #. translators: %s: error message msgid "Error scheduling dry run: %s" msgstr "" #: includes/class-wc-payments-status.php:291 #: includes/class-wc-payments-status.php:310 #: dist/index.js:1 #: client/deposits/filters/config.js:18 msgid "Completed" msgstr "" #: includes/class-wc-payments-status.php:295 #: includes/class-wc-payments-status.php:314 msgid "Running..." msgstr "" #: includes/class-wc-payments-status.php:298 #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:219 #: includes/multi-currency/client/components/preview-modal/index.js:35 #: includes/multi-currency/client/settings/single-currency/index.js:139 #: includes/multi-currency/client/setup/tasks/store-settings-task/index.js:188 msgid "Preview" msgstr "" #: includes/class-wc-payments-status.php:317 msgid "Run" msgstr "" #: includes/class-wc-payments-status.php:326 msgid "" "This tool removes incorrect refund records and fee data from orders where " "payment authorization was canceled (not captured). This fixes negative " "values appearing in WooCommerce Analytics for stores using manual capture." msgstr "" #: includes/class-wc-payments-status.php:338 #. translators: 1: base description, 2: number of orders processed, 3: number #. of orders remediated msgid "" "%1$s <strong>Status: Completed.</strong> Processed %2$d orders, remediated " "%3$d." msgstr "" #: includes/class-wc-payments-status.php:347 #. translators: %s: base description msgid "%s <strong>Status: Completed.</strong> No affected orders found." msgstr "" #: includes/class-wc-payments-status.php:359 #. translators: 1: base description, 2: number of orders processed so far msgid "" "%1$s <strong>Status: Running...</strong> Processed %2$d orders so far. " "Check the Action Scheduler for details." msgstr "" #: includes/class-wc-payments-status.php:367 #. translators: %s: base description msgid "" "%s <strong>Status: Running...</strong> Check the Action Scheduler for " "details." msgstr "" #: includes/class-wc-payments-status.php:419 msgid "Version" msgstr "" #: includes/class-wc-payments-status.php:423 #. translators: %s: WooPayments msgid "The current version of the %s extension." msgstr "" #: includes/class-wc-payments-status.php:429 msgid "Connected to WPCOM" msgstr "" #: includes/class-wc-payments-status.php:433 #. translators: %s: WooPayments msgid "" "Can your store connect securely to wordpress.com? Without a proper WPCOM " "connection %s can't function!" msgstr "" #: includes/class-wc-payments-status.php:436 #: includes/class-wc-payments-status.php:495 #: dist/index.js:1 #: client/merchant-feedback-prompt/index.tsx:79 msgid "Yes" msgstr "" #: includes/class-wc-payments-status.php:436 #: includes/class-wc-payments-status.php:495 #: dist/index.js:1 #: client/merchant-feedback-prompt/index.tsx:87 msgid "No" msgstr "" #: includes/class-wc-payments-status.php:440 msgid "WPCOM Blog ID" msgstr "" #: includes/class-wc-payments-status.php:441 msgid "The corresponding wordpress.com blog ID for this store." msgstr "" #: includes/class-wc-payments-status.php:445 msgid "Account ID" msgstr "" #: includes/class-wc-payments-status.php:446 msgid "The merchant account ID you are currently using to process payments with." msgstr "" #: includes/class-wc-payments-status.php:447 msgid "Not connected" msgstr "" #: includes/class-wc-payments-status.php:454 msgid "Payment Gateway" msgstr "" #: includes/class-wc-payments-status.php:455 msgid "Is the payment gateway ready and enabled for use on your store?" msgstr "" #: includes/class-wc-payments-status.php:456 msgid "Needs setup" msgstr "" #: includes/class-wc-payments-status.php:456 #: includes/class-wc-payments-status.php:461 #: includes/class-wc-payments-status.php:488 #: includes/class-wc-payments-status.php:507 #: includes/class-wc-payments-status.php:562 #: includes/class-wc-payments-status.php:570 #: includes/class-wc-payments-status.php:577 #: includes/class-wc-payments-status.php:584 msgid "Enabled" msgstr "" #: includes/class-wc-payments-status.php:456 #: includes/class-wc-payments-status.php:461 #: includes/class-wc-payments-status.php:488 #: includes/class-wc-payments-status.php:507 #: includes/class-wc-payments-status.php:562 #: includes/class-wc-payments-status.php:570 #: includes/class-wc-payments-status.php:577 #: includes/class-wc-payments-status.php:584 msgid "Disabled" msgstr "" #: includes/class-wc-payments-status.php:459 #: dist/blocks-checkout.js:1 #: client/checkout/blocks/payment-method-label.js:63 msgid "Test Mode" msgstr "" #: includes/class-wc-payments-status.php:460 msgid "Whether the payment gateway has test payments enabled or not." msgstr "" #: includes/class-wc-payments-status.php:464 msgid "Enabled APMs" msgstr "" #: includes/class-wc-payments-status.php:465 msgid "What payment methods are enabled for the store." msgstr "" #: includes/class-wc-payments-status.php:471 #: includes/class-wc-payments-status.php:482 msgid "WooPay Express Checkout" msgstr "" #: includes/class-wc-payments-status.php:475 #. translators: %s: WooPayments msgid "WooPay is not available, as a %s feature, or the store is not yet eligible." msgstr "" #: includes/class-wc-payments-status.php:478 msgid "Not eligible" msgstr "" #: includes/class-wc-payments-status.php:478 msgid "Not active" msgstr "" #: includes/class-wc-payments-status.php:483 msgid "Whether the new WooPay Express Checkout is enabled or not." msgstr "" #: includes/class-wc-payments-status.php:493 msgid "WooPay Incompatible Extensions" msgstr "" #: includes/class-wc-payments-status.php:494 msgid "" "Whether there are extensions active that are have known incompatibilities " "with the functioning of the new WooPay Express Checkout." msgstr "" #: includes/class-wc-payments-status.php:500 msgid "Apple Pay / Google Pay Express Checkout" msgstr "" #: includes/class-wc-payments-status.php:501 msgid "Whether the store has Payment Request enabled or not." msgstr "" #: includes/class-wc-payments-status.php:512 msgid "Fraud Protection Level" msgstr "" #: includes/class-wc-payments-status.php:513 msgid "The current fraud protection level the payment gateway is using." msgstr "" #: includes/class-wc-payments-status.php:518 msgid "Enabled Fraud Filters" msgstr "" #: includes/class-wc-payments-status.php:519 msgid "The advanced fraud protection filters currently enabled." msgstr "" #: includes/multi-currency/Settings.php:46 #: includes/multi-currency/SettingsOnboardCta.php:49 msgctxt "Settings tab label" msgid "Multi-currency" msgstr "" #: includes/class-wc-payments-status.php:561 msgid "Whether the store has the Multi-currency feature enabled or not." msgstr "" #: includes/class-wc-payments-status.php:565 msgid "Auth and Capture" msgstr "" #: includes/class-wc-payments-status.php:566 msgid "Whether the store has the Auth & Capture feature enabled or not." msgstr "" #: includes/class-wc-payments-status.php:576 msgid "Whether the tax documents section is enabled or not." msgstr "" #: includes/class-wc-payments-status.php:582 msgid "Logging" msgstr "" #: includes/class-wc-payments-status.php:583 msgid "Whether debug logging is enabled and working or not." msgstr "" #: includes/class-wc-payments-token-service.php:436 #: includes/class-wc-payments-token-service.php:500 msgid "SEPA IBAN" msgstr "" #: includes/class-wc-payments-token-service.php:452 msgid "Stripe Link email" msgstr "" #: includes/class-wc-payments-utils.php:249 msgid "United Arab Emirates" msgstr "" #: includes/class-wc-payments-utils.php:250 msgid "Austria" msgstr "" #: includes/class-wc-payments-utils.php:251 msgid "Australia" msgstr "" #: includes/class-wc-payments-utils.php:252 msgid "Belgium" msgstr "" #: includes/class-wc-payments-utils.php:253 msgid "Bulgaria" msgstr "" #: includes/class-wc-payments-utils.php:254 msgid "Canada" msgstr "" #: includes/class-wc-payments-utils.php:255 msgid "Switzerland" msgstr "" #: includes/class-wc-payments-utils.php:256 msgid "Cyprus" msgstr "" #: includes/class-wc-payments-utils.php:257 msgid "Czech Republic" msgstr "" #: includes/class-wc-payments-utils.php:258 msgid "Germany" msgstr "" #: includes/class-wc-payments-utils.php:259 msgid "Denmark" msgstr "" #: includes/class-wc-payments-utils.php:260 msgid "Estonia" msgstr "" #: includes/class-wc-payments-utils.php:261 msgid "Finland" msgstr "" #: includes/class-wc-payments-utils.php:262 msgid "Spain" msgstr "" #: includes/class-wc-payments-utils.php:263 msgid "France" msgstr "" #: includes/class-wc-payments-utils.php:264 msgid "Croatia" msgstr "" #: includes/class-wc-payments-utils.php:265 msgid "Japan" msgstr "" #: includes/class-wc-payments-utils.php:266 msgid "Luxembourg" msgstr "" #: includes/class-wc-payments-utils.php:267 msgid "United Kingdom (UK)" msgstr "" #: includes/class-wc-payments-utils.php:268 msgid "Greece" msgstr "" #: includes/class-wc-payments-utils.php:269 msgid "Hong Kong" msgstr "" #: includes/class-wc-payments-utils.php:270 msgid "Hungary" msgstr "" #: includes/class-wc-payments-utils.php:271 msgid "Ireland" msgstr "" #: includes/class-wc-payments-utils.php:272 msgid "Italy" msgstr "" #: includes/class-wc-payments-utils.php:273 msgid "Lithuania" msgstr "" #: includes/class-wc-payments-utils.php:274 msgid "Latvia" msgstr "" #: includes/class-wc-payments-utils.php:275 msgid "Malta" msgstr "" #: includes/class-wc-payments-utils.php:276 msgid "Netherlands" msgstr "" #: includes/class-wc-payments-utils.php:277 msgid "Norway" msgstr "" #: includes/class-wc-payments-utils.php:278 msgid "New Zealand" msgstr "" #: includes/class-wc-payments-utils.php:279 msgid "Poland" msgstr "" #: includes/class-wc-payments-utils.php:280 msgid "Portugal" msgstr "" #: includes/class-wc-payments-utils.php:281 msgid "Romania" msgstr "" #: includes/class-wc-payments-utils.php:282 msgid "Sweden" msgstr "" #: includes/class-wc-payments-utils.php:283 msgid "Slovenia" msgstr "" #: includes/class-wc-payments-utils.php:284 msgid "Slovakia" msgstr "" #: includes/class-wc-payments-utils.php:285 msgid "Singapore" msgstr "" #: includes/class-wc-payments-utils.php:286 msgid "United States (US)" msgstr "" #: includes/class-wc-payments-utils.php:287 msgid "Puerto Rico" msgstr "" #: includes/class-wc-payments-utils.php:310 #: includes/wc-payment-api/class-wc-payments-api-client.php:399 #: includes/wc-payment-api/class-wc-payments-api-client.php:604 #: dist/index.js:1 #: client/disputes/index.tsx:109 #: client/transactions/list/index.tsx:159 msgid "Order #" msgstr "" #: includes/class-wc-payments-utils.php:320 #: includes/wc-payment-api/class-wc-payments-api-client.php:397 #: includes/wc-payment-api/class-wc-payments-api-client.php:602 #: dist/index.js:1 #: client/transactions/list/index.tsx:165 msgid "Subscription #" msgstr "" #: includes/class-wc-payments-utils.php:668 msgid "" "There was an error while processing this request. If you continue to see " "this notice, please contact the admin." msgstr "" #: includes/class-wc-payments-utils.php:680 #. translators: %s a formatted price. msgid "The selected payment method requires a total amount of at least %s." msgstr "" #: includes/class-wc-payments-utils.php:689 #: includes/class-wc-payments-utils.php:691 msgid "" "We're not able to process this request. Please refresh the page and try " "again." msgstr "" #: includes/class-wc-payments-utils.php:693 msgid "" "We couldn’t verify the postal code in your billing address. Make sure the " "information is current with your card issuing bank and try again." msgstr "" #: includes/class-wc-payments-utils.php:1212 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:11 msgid "Bank cannot process" msgstr "" #: includes/class-wc-payments-utils.php:1214 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:14 msgid "Check returned" msgstr "" #: includes/class-wc-payments-utils.php:1216 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:17 msgid "Credit not processed" msgstr "" #: includes/class-wc-payments-utils.php:1218 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:35 msgid "Customer initiated" msgstr "" #: includes/class-wc-payments-utils.php:1220 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:38 msgid "Debit not authorized" msgstr "" #: includes/class-wc-payments-utils.php:1222 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:41 msgid "Duplicate" msgstr "" #: includes/class-wc-payments-utils.php:1224 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:61 msgid "Transaction unauthorized" msgstr "" #: includes/class-wc-payments-utils.php:1226 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:89 msgid "Incorrect account details" msgstr "" #: includes/class-wc-payments-utils.php:1228 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:92 msgid "Insufficient funds" msgstr "" #: includes/class-wc-payments-utils.php:1230 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:95 msgid "Product not received" msgstr "" #: includes/class-wc-payments-utils.php:1232 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:113 msgid "Product unacceptable" msgstr "" #: includes/class-wc-payments-utils.php:1234 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:134 msgid "Subscription canceled" msgstr "" #: includes/class-wc-payments-utils.php:1236 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:153 msgid "Unrecognized" msgstr "" #: includes/class-wc-payments-utils.php:1238 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:170 msgid "Non-compliant" msgstr "" #: includes/class-wc-payments-utils.php:1241 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:80 #: client/settings/settings-manager/index.js:58 msgid "General" msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:300 #: includes/class-wc-payments-webhook-processing-service.php:366 #: includes/class-wc-payments-webhook-processing-service.php:580 #: includes/class-wc-payments-webhook-processing-service.php:610 #: includes/class-wc-payments-webhook-processing-service.php:640 #: includes/class-wc-payments-webhook-processing-service.php:888 #. translators: %1: charge ID msgid "Could not find order via charge ID: %1$s" msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:648 msgid "Payment dispute and fees have been deducted from your next payout" msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:651 msgid "Payment dispute funds have been reinstated" msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:654 msgid "Payment dispute has been updated" msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:659 #. translators: %1: the dispute message, %2: the dispute details URL msgid "%1$s. See <a href=\"%2$s\">dispute overview</a> for more details." msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:712 #. translators: %1: ID being fetched msgid "%1$s not found in array" msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:793 #. translators: %1: intent ID msgid "Could not find order via intent ID: %1$s" msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:818 msgid "The customer's bank account has been closed." msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:820 msgid "The customer has notified their bank that this payment was unauthorized." msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:822 msgid "The customer's account has insufficient funds to cover this payment." msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:824 msgid "The customer's bank account could not be located." msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:826 msgid "" "Microdeposit transfers failed. Please check the account, institution and " "transit numbers." msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:828 msgid "You have exceeded the number of allowed verification attempts." msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:830 msgid "" "The mandate used for this renewal payment is invalid. You may need to bring " "the customer back to your store and ask them to resubmit their payment " "information." msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:834 msgid "The customer's bank could not send pre-debit notification for the payment." msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:836 msgid "" "For recurring payment greater than mandate amount or INR 15000, payment was " "not approved by the card holder." msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:841 #. translators: %s Stripe error message. msgid "With the following message: <code>%s</code>" msgstr "" #: includes/class-wc-payments-webhook-processing-service.php:909 #. translators: %1: charge ID msgid "The refund amount is not valid for charge ID: %1$s" msgstr "" #: includes/class-wc-payments-woopay-button-handler.php:199 #: includes/class-wc-payments.php:1807 includes/class-woopay-tracker.php:95 #: includes/woopay/class-woopay-session.php:631 #: includes/woopay/class-woopay-session.php:678 #: includes/woopay/class-woopay-session.php:704 #: includes/woopay/class-woopay-session.php:747 msgid "You aren’t authorized to do that." msgstr "" #: includes/class-wc-payments.php:1585 #. translators: %1$s: WooCommerce, %2$s: WooPayments, a1: documentation URL msgid "" "The %1$s version you have installed is not compatible with %2$s for a " "Norwegian business. Please update %1$s to version 7.5 or above. You can do " "that via the <a1>the plugins page.</a1>" msgstr "" #: includes/class-wc-payments.php:1874 msgid "Contact information" msgstr "" #: includes/class-wc-payments.php:1881 #: dist/index.js:1 #: dist/settings.js:1 #: client/connect-account-page/strings.tsx:113 #: client/settings/notification-settings/notifications-email-input.tsx:46 msgid "Email address" msgstr "" #: includes/class-wc-payments.php:2066 #. translators: %1$s: WooCommerce , %2$s: WooPayments, %3$s: The current #. WooCommerce version used by the store msgid "" "The %1$s version you have installed is not compatible with %2$s for a " "Hungarian business. Please update %1$s to version 7.8 or above (you are " "using %3$s). You can do that via the <a1>the plugins page.</a1>" msgstr "" #: includes/class-wc-payments.php:2070 #. translators: %1$s: WooCommerce , %2$s: WooPayments, %3$s: The current #. WooCommerce version used by the store msgid "" "The %1$s version you have installed is not compatible with %2$s for a " "Swedish business. Please update %1$s to version 7.8 or above (you are using " "%3$s). You can do that via the <a1>the plugins page.</a1>" msgstr "" #: includes/class-wc-payments.php:2074 #. translators: %1$s: WooCommerce , %2$s: WooPayments, %3$s: The current #. WooCommerce version used by the store msgid "" "The %1$s version you have installed is not compatible with %2$s for a Czech " "Republic business. Please update %1$s to version 7.8 or above (you are " "using %3$s). You can do that via the <a1>the plugins page.</a1>" msgstr "" #: includes/class-wc-payments.php:2118 msgid "Mobile Number" msgstr "" #: includes/class-wc-payments.php:2118 msgid "is required to create an WooPay account." msgstr "" #: includes/class-woopay-tracker.php:102 msgid "No valid event name or type." msgstr "" #: includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php:54 msgid "Payment authentication requested email" msgstr "" #: includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php:55 msgid "" "Payment authentication requested emails are sent to chosen recipient(s) " "when an attempt to automatically process a subscription renewal payment " "fails because the transaction requires an SCA verification, the customer is " "requested to authenticate the payment, and a retry rule has been applied to " "notify the customer again within a certain time period." msgstr "" #: includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php:57 msgid "Automatic renewal payment failed due to authentication required" msgstr "" #: includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php:58 msgid "" "[{site_title}] Automatic payment failed for {order_number}. Customer asked " "to authenticate payment and will be notified again {retry_time}" msgstr "" #: includes/compat/subscriptions/class-wc-payments-email-failed-renewal-authentication.php:32 msgid "Failed subscription renewal SCA authentication" msgstr "" #: includes/compat/subscriptions/class-wc-payments-email-failed-renewal-authentication.php:33 msgid "" "Sent to a customer when a renewal fails because the transaction requires an " "SCA verification. The email contains renewal order information and payment " "links." msgstr "" #: includes/compat/subscriptions/class-wc-payments-email-failed-renewal-authentication.php:118 msgid "Enable this email notification" msgstr "" #: includes/compat/subscriptions/class-wc-payments-email-failed-renewal-authentication.php:134 msgid "" "Payment authorization needed for renewal of {site_title} order " "{order_number}" msgstr "" #: includes/compat/subscriptions/class-wc-payments-email-failed-renewal-authentication.php:143 msgid "Payment authorization needed for renewal of order {order_number}" msgstr "" #: includes/compat/subscriptions/emails/failed-renewal-authentication-requested.php:34 #: includes/compat/subscriptions/emails/plain/failed-renewal-authentication-requested.php:27 msgid "The renewal order is as follows:" msgstr "" #: includes/compat/subscriptions/emails/failed-renewal-authentication.php:18 msgid "Authorize the payment »" msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:329 msgid "" "Almost there!\n" "\n" "Your order has already been created, the only thing that still needs to be " "done is for you to authorize the payment with your bank." msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:442 msgid "Saved payment method" msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:513 msgid "A customer saved payment method was not selected for this order." msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:522 msgid "The saved payment method selected is invalid or does not exist." msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:529 msgid "The saved payment method selected does not belong to this order's customer." msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:686 msgid "You do not have permission to perform this action." msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:700 msgid "Invalid user ID." msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:749 msgid "No payment methods found for customer" msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:753 #: dist/subscription-edit-page.js:1 #: client/subscription-edit-page/index.tsx:136 msgid "Please select a customer first" msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:761 #: dist/subscription-edit-page.js:1 #: client/subscription-edit-page/index.tsx:147 msgid "Please select a payment method" msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:798 #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:817 #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:858 #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:866 #. translators: 1: payment method likely credit card, 2: last 4 digit. msgid "%1$s ending in %2$s" msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:1133 #. translators: 1) date in date_format or 'F j, Y'; 2) time in time_format or #. 'g:i a' msgid "" "The customer must authorize this payment via a notification sent to them by " "the bank which issued their card. The authorization must be completed " "before %1$s at %2$s, when the charge will be attempted." msgstr "" #: includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php:1210 #. translators: %s: payment method type msgid "" "Subscription set to manual renewal because %s is a non-reusable payment " "method." msgstr "" #: includes/constants/class-refund-failure-reason.php:37 msgid "The card used for the original payment has been reported lost or stolen." msgstr "" #: includes/constants/class-refund-failure-reason.php:38 msgid "The card used for the original payment has expired or been canceled." msgstr "" #: includes/constants/class-refund-failure-reason.php:39 msgid "The charge for this refund is being disputed by the customer." msgstr "" #: includes/constants/class-refund-failure-reason.php:40 msgid "Insufficient funds in your WooPayments balance." msgstr "" #: includes/constants/class-refund-failure-reason.php:41 msgid "The refund was declined by the card issuer." msgstr "" #: includes/constants/class-refund-failure-reason.php:42 msgid "The refund was canceled at your request." msgstr "" #: includes/constants/class-refund-failure-reason.php:43 msgid "An unknown error occurred while processing the refund." msgstr "" #: includes/core/server/class-request.php:671 msgid "Empty parameter is not allowed" msgstr "" #: includes/core/server/class-request.php:702 #. Translators: %s is a Stripe ID. msgid "%s is not a valid Stripe identifier" msgstr "" #: includes/core/server/class-request.php:727 #. translators: %1$s and %2$s are both numbers msgid "Invalid number passed. Number %1$s needs to be larger than %2$s" msgstr "" #: includes/core/server/class-request.php:751 #. Translators: %s is a currency code. msgid "%s is not a supported currency for payments." msgstr "" #: includes/core/server/class-request.php:801 #. Translators: %1$s is a provided date string, %2$s is a date format. msgid "%1$s is not a valid date for format %2$s." msgstr "" #: includes/core/server/class-request.php:826 #. Translators: %s is a currency code. msgid "" "%1$s is not a valid redirect URL. Use a URL in the allowed_redirect_hosts " "filter." msgstr "" #: includes/core/server/class-request.php:850 #. Translators: %s is a provided username. msgid "%s is not a valid username." msgstr "" #: includes/core/server/request/class-create-and-confirm-setup-intention.php:121 msgid "Intentions require at least one payment method" msgstr "" #: includes/core/server/request/class-list-disputes.php:137 msgid "The search parameter must be a string, or an array of strings." msgstr "" #: includes/core/server/request/class-update-account.php:61 msgid "No account settings provided" msgstr "" #: includes/emails/class-wc-payments-email-ipp-receipt.php:60 msgid "New receipt" msgstr "" #: includes/emails/class-wc-payments-email-ipp-receipt.php:61 msgid "" "New receipt emails are sent to customers when a new order is paid for with " "a card reader." msgstr "" #: includes/emails/class-wc-payments-email-ipp-receipt.php:149 msgid "Your {site_title} Receipt" msgstr "" #: includes/emails/class-wc-payments-email-ipp-receipt.php:159 msgid "Your receipt for order: #{order_number}" msgstr "" #: includes/emails/class-wc-payments-email-ipp-receipt.php:367 msgid "Thanks for using {site_url}!" msgstr "" #: includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php:85 msgid "Invalid product id" msgstr "" #: includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php:115 msgid "OR" msgstr "" #: includes/express-checkout/class-wc-payments-express-checkout-button-handler.php:146 #. translators: The text encapsulated in `**` can be replaced with "Apple Pay" #. or "Google Pay". Please translate this text, but don't remove the `**`. msgid "" "To complete your transaction with **the selected payment method**, you must " "log in or create an account with our site." msgstr "" #: includes/express-checkout/class-wc-payments-express-checkout-button-helper.php:121 #: includes/express-checkout/class-wc-payments-express-checkout-button-helper.php:709 #: dist/express-checkout.js:1 #: dist/blocks-checkout.js:1 #: client/express-checkout/transformers/wc-to-stripe.js:105 msgid "Shipping" msgstr "" #: includes/express-checkout/class-wc-payments-express-checkout-button-helper.php:716 #: dist/index.js:1 #: dist/blocks-checkout.js:1 #: dist/express-checkout.js:1 #: client/deposits/strings.ts:18 #: client/express-checkout/blocks/hooks/use-express-checkout.js:93 #: client/express-checkout/index.js:337 msgid "Pending" msgstr "" #: includes/express-checkout/class-wc-payments-express-checkout-button-helper.php:856 #. Translators: %d is the numeric ID of the product without a price. msgid "" "Express checkout does not support products without prices! Please add a " "price to product #%d" msgstr "" #: includes/fraud-prevention/class-fraud-risk-tools.php:105 #: dist/settings.js:1 #: client/settings/settings-manager/index.js:115 msgid "Fraud protection" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:57 msgid "Fraud & Risk" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:89 msgid "Green check mark" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:93 msgid "Orange shield outline" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:97 msgid "Red shield outline" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:102 #: dist/index.js:1 #: client/transactions/index.tsx:70 msgid "Blocked" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:103 msgid "Approved" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:104 msgid "Held for review" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:105 msgid "No action taken" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:108 msgid "Adjust risk filters" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:118 msgid "The payment for this order passed your risk filtering." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:123 msgid "" "The payment for this order was blocked by your risk filtering. There is no " "pending authorization, and the order can be cancelled to reduce any held " "stock." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:124 msgid "View more details" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:137 #. translators: %1: WooPayments, %2: Payment method title msgid "" "Risk filtering is only available for orders processed using credit cards " "with %1$s. This order was processed with %2$s." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:144 #. translators: %s: WooPayments msgid "" "Risk filtering is only available for orders processed using credit cards " "with %s." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:149 #: dist/index.js:1 #: dist/settings.js:1 #: client/disputes/new-evidence/index.tsx:407 #: client/onboarding/steps/embedded-kyc.tsx:65 #: client/payment-details/dispute-details/dispute-steps.tsx:16 #: client/payment-details/dispute-details/dispute-summary-row.tsx:42 #: client/settings/buy-now-pay-later-section/index.js:31 #: client/settings/notification-settings/index.tsx:17 #: client/settings/settings-manager/index.js:51 #: client/utils/account-fees.tsx:130 msgid "Learn more" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:156 msgid "" "The payment for this order has not yet been passed to the fraud and risk " "filters to determine its outcome status." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:161 msgid "" "The payment for this order was held for review by your risk filtering. You " "can review the details and determine whether to approve or block the " "payment." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:162 #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:174 #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:181 #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:188 msgid "Review payment" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:168 msgid "" "The payment for this order was held for review by your risk filtering and " "manually approved." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:173 msgid "" "This transaction was held for review by your risk filters, and the charge " "was manually blocked after review." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:180 msgid "" "The payment for this order was held for review by your risk filtering. The " "authorization for the charge appears to have expired." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:187 msgid "" "The payment for this order was held for review by your risk filtering. The " "authorization for the charge appears to have failed." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:194 msgid "" "The payment for this order was done in person and has bypassed your risk " "filtering." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:201 #. translators: %s: WooPayments msgid "" "Risk filtering through %s was not found on this order, it may have been " "created while filtering was not enabled." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:230 #: dist/index.js:1 #: client/components/risk-level/strings.ts:7 #: client/transactions/strings.ts:34 msgid "Normal" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:231 #: dist/index.js:1 #: client/components/risk-level/strings.ts:8 #: client/transactions/strings.ts:35 msgid "Elevated" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:232 msgid "High" msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:236 msgid "This payment shows a lower than normal risk of fraudulent activity." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:237 msgid "" "This order has a moderate risk of being fraudulent. We suggest contacting " "the customer to confirm their details before fulfilling it." msgstr "" #: includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php:238 msgid "" "This order has a high risk of being fraudulent. We suggest contacting the " "customer to confirm their details before fulfilling it." msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:132 #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:414 #: client/payment-details/summary/index.tsx:122 #: client/transactions/uncaptured/index.tsx:46 msgid "Order" msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:143 msgid "SKU" msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:154 msgid "SUBTOTAL" msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:168 msgid "Fees:" msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:176 msgid "Shipping:" msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:195 msgid "TOTAL" msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:204 msgid "AMOUNT PAID" msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:214 msgid "Application name" msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:215 #: templates/emails/email-ipp-receipt-compliance-details.php:42 #: templates/emails/plain/email-ipp-receipt-compliance-details.php:23 msgid "AID" msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:216 #: templates/emails/email-ipp-receipt-compliance-details.php:50 #: templates/emails/plain/email-ipp-receipt-compliance-details.php:25 msgid "Account Type" msgstr "" #: includes/in-person-payments/templates/html-in-person-payment-receipt.php:217 msgid "Powered by WooCommerce" msgstr "" #: includes/multi-currency/AdminNotices.php:72 msgid "Action failed. Please refresh the page and retry." msgstr "" #: includes/multi-currency/AdminNotices.php:76 msgid "Cheatin’ huh?" msgstr "" #: includes/multi-currency/AdminNotices.php:112 #. translators: %s List of currencies that are already translated in #. WooCommerce core. msgid "" "The store currency was recently changed. The following currencies are set " "to manual rates and may need updates: %s" msgstr "" #: includes/multi-currency/CurrencySwitcherWidget.php:50 msgid "Currency Switcher Widget" msgstr "" #: includes/multi-currency/CurrencySwitcherWidget.php:51 #: includes/multi-currency/client/blocks/currency-switcher.js:31 msgid "Let your customers switch between your enabled currencies" msgstr "" #: includes/multi-currency/CurrencySwitcherWidget.php:56 msgid "Title" msgstr "" #: includes/multi-currency/CurrencySwitcherWidget.php:61 #: includes/multi-currency/client/blocks/currency-switcher.js:238 msgid "Display currency symbols" msgstr "" #: includes/multi-currency/CurrencySwitcherWidget.php:66 msgid "Display flags in supported devices" msgstr "" #: includes/multi-currency/MultiCurrency.php:967 #. translators: %1 User's country, %2 Selected currency name, %3 Default store #. currency name, %4 Link to switch currency msgid "" "We noticed you're visiting from %1$s. We've updated our prices to %2$s for " "your shopping convenience. <a href=\"%4$s\">Use %3$s instead.</a>" msgstr "" #: includes/multi-currency/MultiCurrency.php:980 #: dist/index.js:1 #: client/merchant-feedback-prompt/index.tsx:89 #: client/overview/modal/connection-success/strings.tsx:7 msgid "Dismiss" msgstr "" #: includes/multi-currency/Notes/NoteMultiCurrencyAvailable.php:46 msgid "Sell worldwide in multiple currencies" msgstr "" #: includes/multi-currency/Notes/NoteMultiCurrencyAvailable.php:47 msgid "" "Boost your international sales by allowing your customers to shop and pay " "in their local currency." msgstr "" #: includes/multi-currency/Notes/NoteMultiCurrencyAvailable.php:54 #: includes/notes/class-wc-payments-notes-set-up-stripelink.php:80 msgid "Set up now" msgstr "" #: includes/multi-currency/SettingsOnboardCta.php:74 #. translators: %s: WooPayments msgid "To add new currencies to your store, please finish setting up %s." msgstr "" #: includes/multi-currency/SettingsOnboardCta.php:80 msgid "Get started" msgstr "" #: includes/multi-currency/SettingsOnboardCta.php:99 #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/index.js:28 #: includes/multi-currency/client/settings/single-currency/index.js:188 msgid "Enabled currencies" msgstr "" #: includes/multi-currency/SettingsOnboardCta.php:102 #. translators: %s: url to documentation. msgid "" "Accept payments in multiple currencies. Prices are converted based on " "exchange rates and rounding rules. <a href=\"%s\">Learn more</a>" msgstr "" #: includes/multi-currency/UserSettings.php:54 #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/list-item.js:36 msgid "Default currency" msgstr "" #: includes/multi-currency/UserSettings.php:69 msgid "Select your preferred currency for shopping and payments." msgstr "" #: includes/notes/class-wc-payments-notes-canceled-auth-remediation.php:68 msgid "WooPayments: Fix incorrect order data" msgstr "" #: includes/notes/class-wc-payments-notes-canceled-auth-remediation.php:70 msgid "" "Some orders with canceled payment authorizations have incorrect data that " "may cause negative values in your WooCommerce Analytics. This affects " "stores using manual capture (authorize and capture separately). Run the fix " "tool to correct this." msgstr "" #: includes/notes/class-wc-payments-notes-canceled-auth-remediation.php:81 msgid "Go to Tools page" msgstr "" #: includes/notes/class-wc-payments-notes-instant-deposits-eligible.php:33 #. translators: %s: WooPayments msgid "You’re now eligible to receive Instant Payouts with %s" msgstr "" #: includes/notes/class-wc-payments-notes-instant-deposits-eligible.php:41 #. translators: %s: WooPayments msgid "" "Get immediate access to your funds when you need them – including nights, " "weekends, and holidays. With %s' <a>Instant Payouts feature</a>, you're " "able to transfer your earnings to a debit card within minutes." msgstr "" #: includes/notes/class-wc-payments-notes-instant-deposits-eligible.php:53 msgid "Request an instant payout" msgstr "" #: includes/notes/class-wc-payments-notes-loan-approved.php:46 msgid "Your capital loan has been approved!" msgstr "" #: includes/notes/class-wc-payments-notes-loan-approved.php:50 #. Translators: %1: total amount lent to the merchant formatted in the account #. currency, %2: WooPayments msgid "" "Congratulations! Your capital loan has been approved and %1$s was deposited " "into the bank account linked to %2$s. You'll automatically repay the loan, " "plus a flat fee, through a fixed percentage of each %2$s transaction." msgstr "" #: includes/notes/class-wc-payments-notes-loan-approved.php:76 msgid "View loan details" msgstr "" #: includes/notes/class-wc-payments-notes-qualitative-feedback.php:70 #. translators: %s: WooPayments msgid "Help us make improvements to %s" msgstr "" #: includes/notes/class-wc-payments-notes-qualitative-feedback.php:74 msgid "" "Share your feedback in this 2 minute survey about how we can make the " "process of accepting payments more useful for your store." msgstr "" #: includes/notes/class-wc-payments-notes-qualitative-feedback.php:81 msgid "Share feedback" msgstr "" #: includes/notes/class-wc-payments-notes-set-https-for-checkout.php:51 msgid "Enable secure checkout" msgstr "" #: includes/notes/class-wc-payments-notes-set-https-for-checkout.php:52 msgid "" "Enable HTTPS on your checkout pages to display all available payment " "methods and protect your customers data." msgstr "" #: includes/notes/class-wc-payments-notes-set-https-for-checkout.php:59 #: dist/settings.js:1 #: client/settings/express-checkout/link-item.tsx:73 msgid "Read more" msgstr "" #: includes/notes/class-wc-payments-notes-set-up-stripelink.php:72 msgid "Increase conversion at checkout" msgstr "" #: includes/notes/class-wc-payments-notes-set-up-stripelink.php:73 msgid "" "Reduce cart abandonment and create a frictionless checkout experience with " "Link by Stripe. Link autofills your customer’s payment and shipping " "details, so they can check out in just six seconds with the Link optimized " "experience." msgstr "" #: includes/notes/class-wc-payments-notes-stripe-billing-deprecation.php:56 msgid "Built-in subscriptions functionality has been removed. Here's what to do" msgstr "" #: includes/notes/class-wc-payments-notes-stripe-billing-deprecation.php:57 msgid "" "To continue offering subscriptions and gain access to your data, please " "install WooCommerce Subscriptions. WooPayments no longer supports this " "feature." msgstr "" #: includes/notes/class-wc-payments-notes-stripe-billing-deprecation.php:62 msgid "Install WooCommerce Subscriptions" msgstr "" #: includes/payment-methods/Configs/Definitions/AffirmDefinition.php:56 msgid "Affirm" msgstr "" #: includes/payment-methods/Configs/Definitions/AffirmDefinition.php:77 msgid "Allow customers to pay over time with Affirm." msgstr "" #: includes/payment-methods/Configs/Definitions/AfterpayDefinition.php:58 msgid "Clearpay" msgstr "" #: includes/payment-methods/Configs/Definitions/AfterpayDefinition.php:62 msgid "Cash App Afterpay" msgstr "" #: includes/payment-methods/Configs/Definitions/AfterpayDefinition.php:65 msgid "Afterpay" msgstr "" #: includes/payment-methods/Configs/Definitions/AfterpayDefinition.php:87 msgid "Allow customers to pay over time with Clearpay." msgstr "" #: includes/payment-methods/Configs/Definitions/AfterpayDefinition.php:91 msgid "Allow customers to pay over time with Cash App Afterpay." msgstr "" #: includes/payment-methods/Configs/Definitions/AfterpayDefinition.php:94 msgid "Allow customers to pay over time with Afterpay." msgstr "" #: includes/payment-methods/Configs/Definitions/AlipayDefinition.php:56 msgid "Alipay" msgstr "" #: includes/payment-methods/Configs/Definitions/AlipayDefinition.php:77 msgid "" "A digital wallet for customers with mainland China Alipay accounts. " "Regional versions like AlipayHK are not supported." msgstr "" #: includes/payment-methods/Configs/Definitions/AmazonPayDefinition.php:77 msgid "Offer customers a fast, secure checkout experience with Amazon Pay." msgstr "" #: includes/payment-methods/Configs/Definitions/ApplePayDefinition.php:54 msgid "Apple Pay" msgstr "" #: includes/payment-methods/Configs/Definitions/ApplePayDefinition.php:75 msgid "Apple Pay is an easy and secure way for customers to pay on your store." msgstr "" #: includes/payment-methods/Configs/Definitions/BancontactDefinition.php:56 msgid "Bancontact" msgstr "" #: includes/payment-methods/Configs/Definitions/BancontactDefinition.php:77 msgid "" "Bancontact is a bank redirect payment method offered by more than 80% of " "online businesses in Belgium." msgstr "" #: includes/payment-methods/Configs/Definitions/BecsDefinition.php:56 msgid "BECS Direct Debit" msgstr "" #: includes/payment-methods/Configs/Definitions/BecsDefinition.php:77 msgid "" "Bulk Electronic Clearing System — Accept secure bank transfer from " "Australia." msgstr "" #: includes/payment-methods/Configs/Definitions/BecsDefinition.php:154 msgid "" "<strong>Test mode:</strong> use the test account number " "<number>000123456</number>. Other payment methods may redirect to a Stripe " "test page to authorize payment. More test card numbers are listed " "<a>here</a>." msgstr "" #: includes/payment-methods/Configs/Definitions/EpsDefinition.php:56 msgid "EPS" msgstr "" #: includes/payment-methods/Configs/Definitions/EpsDefinition.php:77 msgid "Accept your payment with EPS — a common payment method in Austria." msgstr "" #: includes/payment-methods/Configs/Definitions/GiropayDefinition.php:56 msgid "giropay" msgstr "" #: includes/payment-methods/Configs/Definitions/GiropayDefinition.php:77 msgid "" "Expand your business with giropay — Germany's second most popular payment " "system." msgstr "" #: includes/payment-methods/Configs/Definitions/GooglePayDefinition.php:54 msgid "Google Pay" msgstr "" #: includes/payment-methods/Configs/Definitions/GooglePayDefinition.php:75 msgid "Offer customers a fast, secure checkout experience with Google Pay." msgstr "" #: includes/payment-methods/Configs/Definitions/GrabPayDefinition.php:56 msgid "GrabPay" msgstr "" #: includes/payment-methods/Configs/Definitions/GrabPayDefinition.php:77 msgid "A popular digital wallet for cashless payments in Singapore." msgstr "" #: includes/payment-methods/Configs/Definitions/IdealDefinition.php:56 msgid "iDEAL" msgstr "" #: includes/payment-methods/Configs/Definitions/IdealDefinition.php:77 msgid "Expand your business with iDEAL — Netherlands's most popular payment method." msgstr "" #: includes/payment-methods/Configs/Definitions/KlarnaDefinition.php:57 msgid "Klarna" msgstr "" #: includes/payment-methods/Configs/Definitions/KlarnaDefinition.php:78 msgid "Allow customers to pay over time or pay now with Klarna." msgstr "" #: includes/payment-methods/Configs/Definitions/LinkDefinition.php:77 msgid "" "Link autofills your customers' payment and shipping details to deliver an " "easy and seamless checkout experience." msgstr "" #: includes/payment-methods/Configs/Definitions/MultibancoDefinition.php:77 msgid "A voucher based payment method for your customers in Portugal." msgstr "" #: includes/payment-methods/Configs/Definitions/P24Definition.php:56 msgid "Przelewy24 (P24)" msgstr "" #: includes/payment-methods/Configs/Definitions/P24Definition.php:77 msgid "" "Accept payments with Przelewy24 (P24), the most popular payment method in " "Poland." msgstr "" #: includes/payment-methods/Configs/Definitions/SepaDefinition.php:56 msgid "SEPA Direct Debit" msgstr "" #: includes/payment-methods/Configs/Definitions/SepaDefinition.php:77 msgid "" "Reach 500 million customers and over 20 million businesses across the " "European Union." msgstr "" #: includes/payment-methods/Configs/Definitions/SepaDefinition.php:197 msgid "" "<strong>Test mode:</strong> use the test account number " "<number>AT611904300234573201</number>. Other payment methods may redirect " "to a Stripe test page to authorize payment. More test card numbers are " "listed <a>here</a>." msgstr "" #: includes/payment-methods/Configs/Definitions/SofortDefinition.php:56 msgid "Sofort" msgstr "" #: includes/payment-methods/Configs/Definitions/SofortDefinition.php:77 msgid "" "Accept secure bank transfers from Austria, Belgium, Germany, Italy, " "Netherlands, and Spain." msgstr "" #: includes/payment-methods/Configs/Definitions/WechatPayDefinition.php:56 msgid "WeChat Pay" msgstr "" #: includes/payment-methods/Configs/Definitions/WechatPayDefinition.php:77 msgid "" "A digital wallet for customers with mainland China WeChat Pay wallets. " "Regional versions like WeChat Pay HK are not supported." msgstr "" #: includes/payment-methods/class-cc-payment-method.php:42 msgid "Card" msgstr "" #: includes/payment-methods/class-cc-payment-method.php:47 #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:39 #: client/payment-details/payment-method/card/index.js:46 msgid "credit" msgstr "" #: includes/payment-methods/class-cc-payment-method.php:48 #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:40 #: client/payment-details/payment-method/card/index.js:47 msgid "debit" msgstr "" #: includes/payment-methods/class-cc-payment-method.php:49 #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:41 #: client/payment-details/payment-method/card/index.js:48 msgid "prepaid" msgstr "" #: includes/payment-methods/class-cc-payment-method.php:59 #. Translators: %1$s card brand, %2$s card funding (prepaid, credit, etc.). #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:46 #: client/payment-details/payment-method/card/index.js:54 #. %1$s card brand, %2$s card funding (prepaid, credit, etc.). msgid "%1$s %2$s card" msgstr "" #: includes/payment-methods/class-cc-payment-method.php:78 #. Translators: %s is a test card number. msgid "Use test card <number>%s</number> or refer to our <a>testing guide</a>." msgstr "" #: includes/payment-methods/class-cc-payment-method.php:91 msgid "" "Let your customers pay with major credit and debit cards without leaving " "your store." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:171 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:164 msgid "Filter transactions before this date." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:177 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:170 msgid "Filter transactions after this date." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:183 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:176 msgid "Filter transactions between these dates." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:187 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:180 msgid "Filter transactions based on the associated order ID." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:194 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:187 msgid "Filter transactions based on the associated deposit ID." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:200 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:193 msgid "Filter transactions based on the customer email." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:206 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:199 msgid "Filter transactions based on the payment method used." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:212 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:205 msgid "Filter transactions where type is a specific value." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:218 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:211 msgid "Match filter for the transactions." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:223 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:216 msgid "Include timezone into date filtering." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:228 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:221 msgid "Page number." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:235 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:228 msgid "Page size." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:243 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:236 msgid "Field on which to sort." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:249 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:242 msgid "Direction on which to sort." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:270 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:263 msgid "The date and time when the transaction was created." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:276 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:269 msgid "A unique identifier for each transaction based on its transaction type." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:281 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:274 msgid "A unique source id for each transaction." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:286 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:279 msgid "Indicates whether the transaction was made online or offline." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:291 #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:296 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:284 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:289 msgid "" "Specifies whether the payment method used was a card (Visa, Mastercard, " "etc.) or an Alternative Payment Method (APM) or Local Payment Method (LPM) " "(iDEAL, Apple Pay, Google Pay, etc.)." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:301 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:294 msgid "The payment method ID used to create the transaction type." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:308 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:301 msgid "The type of the transaction." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:323 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:316 msgid "The exchange rate of the transaction." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:328 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:321 msgid "The currency of the store." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:333 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:326 msgid "Transaction fees." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:338 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:331 msgid "Customer details." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:343 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:336 msgid "Customer name." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:348 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:341 msgid "Customer email." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:353 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:346 msgid "Customer country." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:360 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:353 msgid "Net amount." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:365 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:358 msgid "The identifier of the WooCommerce order associated with this transaction." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:370 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:363 msgid "Fraud risk level." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:375 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:368 msgid "Deposit date of transaction" msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:380 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:373 msgid "A unique identifier for the deposit." msgstr "" #: includes/reports/class-wc-rest-payments-reports-authorizations-controller.php:385 #: includes/reports/class-wc-rest-payments-reports-transactions-controller.php:378 msgid "The status of the deposit" msgstr "" #: includes/subscriptions/class-wc-payments-invoice-service.php:291 msgid "The payment info couldn't be added to the order." msgstr "" #: includes/subscriptions/class-wc-payments-invoice-service.php:438 msgid "The WooPayments invoice items do not match WC subscription items." msgstr "" #: includes/subscriptions/class-wc-payments-product-service.php:618 #. translators: %1$s Opening strong tag, %2$s Closing strong tag, %3$s The #. subscription renewal interval (every x time) msgid "" "%1$sThere was an issue saving your product!%2$s A subscription product's " "billing period cannot be longer than one year. We have updated this product " "to renew every %3$s." msgstr "" #: includes/subscriptions/class-wc-payments-product-service.php:663 #. translators: %1$s Opening strong tag, %2$s Closing strong tag msgid "" "%1$sThere was an issue saving your variations!%2$s A subscription product's " "billing period cannot be longer than one year. We have updated one or more " "of this product's variations to renew every %3$s." msgstr "" #: includes/subscriptions/class-wc-payments-subscription-change-payment-method-handler.php:53 msgid "Update payment method" msgstr "" #: includes/subscriptions/class-wc-payments-subscription-change-payment-method-handler.php:159 msgid "Update payment details" msgstr "" #: includes/subscriptions/class-wc-payments-subscription-change-payment-method-handler.php:175 msgid "" "Your subscription's last renewal failed payment. Please update your payment " "details so we can reattempt payment." msgstr "" #: includes/subscriptions/class-wc-payments-subscription-change-payment-method-handler.php:226 msgid "Update and retry payment" msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:347 #. Translators: %s Coupon code. msgid "Coupon - %s" msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:397 msgid "" "There was a problem creating your subscription. Please try again or contact " "us for assistance." msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:430 #. Translators: The %1 placeholder is a currency formatted price string #. ($0.50). The %2 and %3 placeholders are opening and closing strong HTML #. tags. msgid "" "There was a problem creating your subscription. %1$s doesn't meet the " "%2$sminimum recurring amount%3$s this payment method can process." msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:440 #. Translators: %1$s and %2$s are both currency codes, e.g. `USD` or `EUR`. msgid "" "The subscription couldn't be created because it uses a different currency " "(%1$s) from your existing subscriptions (%2$s). Please ensure all " "subscriptions use the same currency." msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:515 msgid "" "Suspended WooPayments Subscription because subscription status changed to " "on-hold." msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:656 msgid "" "We've successfully collected payment for your subscription using your new " "payment method." msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:730 #. translators: %s: WooPayments msgid "%s Subscription ID" msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:753 msgid "" "The subscription's next payment date has been updated to match WooPayments " "server." msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:907 msgid "" "Subscription was made when WooPayments was in the test mode and cannot be " "renewed in the live mode." msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:909 msgid "" "Subscription was made when WooPayments was in the live mode and cannot be " "renewed in the test mode." msgstr "" #: includes/subscriptions/class-wc-payments-subscription-service.php:1028 #. Translators: %s Stripe subscription item ID. msgid "" "Unable to set subscription item ID meta for WooPayments subscription item " "%s." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-admin-notices.php:48 #. translators: %1$s: WooCommerce Subscriptions msgid "" "<strong>Important:</strong> From version 9.7 of WooPayments (scheduled for " "23 July, 2025), you'll <strong>no longer be able to offer new product " "subscriptions</strong>. To avoid disruption, please install <a " "target=\"_blank\" href=\"%1$s\">WooCommerce Subscriptions</a>." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-admin-notices.php:54 #. translators: %1$s: WooCommerce Subscriptions msgid "" "WooPayments no longer allows customers to create new subscriptions. " "Beginning in version 9.8, billing for existing customer subscriptions will " "no longer be supported. To ensure there is no interruption of service, " "please install <a target=\"_blank\" href=\"%1$s\">WooCommerce " "Subscriptions</a>." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-admin-notices.php:60 #. translators: %1$s: WooCommerce Subscriptions msgid "" "WooPayments no longer supports billing for existing customer subscriptions. " "All subscriptions data is read-only. Please install <a target=\"_blank\" " "href=\"%1$s\">WooCommerce Subscriptions</a> to continue managing your " "subscriptions." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-admin-notices.php:66 #. translators: %1$s: WooCommerce Subscriptions msgid "" "WooPayments no longer supports subscriptions capabilities and subscriptions " "data can no longer be accessed. Please install <a target=\"_blank\" " "href=\"%1$s\">WooCommerce Subscriptions</a> to continue managing your " "subscriptions." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-disabler.php:418 #. translators: %s: product name msgid "" "%s has been removed from your cart because it can no longer be purchased. " "Please contact us if you need assistance." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-disabler.php:478 msgid "" "Subscription products cannot be added to orders. Please install WooCommerce " "Subscriptions to manage subscriptions." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-disabler.php:575 #. translators: %1$s: WooCommerce Subscriptions link msgid "" "To access your subscriptions data and keep managing recurring payments, " "please install <a target=\"_blank\" href=\"%1$s\">WooCommerce " "Subscriptions</a>. Built-in support for subscriptions is no longer " "available in WooPayments." msgstr "" #. Plugin Name of the plugin/theme msgid "WooPayments" msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:79 msgid "Cannot find subscription to handle the \"invoice.upcoming\" event." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:91 msgid "" "Suspended WooPayments Subscription in invoice.upcoming webhook handler " "because subscription next_payment date is 0." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:102 #. Translators: %s Scheduled/upcoming payment date in Y-m-d H:i:s format. msgid "Next automatic payment scheduled for %s." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:138 msgid "Cannot find subscription for the incoming \"invoice.paid\" event." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:152 msgid "" "Unable to generate renewal order for subscription on the \"invoice.paid\" " "event." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:233 msgid "Cannot find subscription for the incoming \"invoice.payment_failed\" event." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:260 msgid "" "Unable to generate renewal order for subscription to record the incoming " "\"invoice.payment_failed\" event." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:271 #. Translators: %1$d Number of failed renewal attempts. %2$s contains failure #. message, %3$s contains error code. msgid "" "WooPayments subscription renewal attempt %1$d failed with the following " "message \"%2$s\" and failure code <code>%3$s</code>" msgid_plural "" "WooPayments subscription renewal attempt %1$d failed with the following " "message \"%2$s\" and failure code <code>%3$s</code>" msgstr[0] "" msgstr[1] "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:285 #. Translators: %1$s contains failure message. %2$s contains error code. msgid "" "Payment for the order failed with the following message: \"%1$s\" and " "failure code <code>%2$s</code>" msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:295 #. Translators: %d Number of failed renewal attempts. msgid "WooPayments subscription renewal attempt %d failed." msgid_plural "WooPayments subscription renewal attempt %d failed." msgstr[0] "" msgstr[1] "" #: includes/subscriptions/class-wc-payments-subscriptions-event-handler.php:330 #. Translators: %s Property name not found in event data array. msgid "%s not found in array" msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-migrator.php:140 msgid "" "This subscription has been successfully migrated to a WooPayments tokenized " "subscription." msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-migrator.php:547 msgid "Migrate Stripe Billing subscriptions" msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-migrator.php:548 msgid "Migration in progress" msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-migrator.php:548 msgid "Migrate Subscriptions" msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-migrator.php:551 #. translators: %1$s is a new line character and %2$d is the number of #. subscriptions. msgid "" "This tool will migrate all Stripe Billing subscriptions to tokenized " "subscriptions with WooPayments.%1$sNumber of Stripe Billing subscriptions " "found: %2$d" msgstr "" #: includes/subscriptions/templates/html-subscriptions-plugin-notice.php:14 #: includes/subscriptions/templates/html-wcpay-deactivate-warning.php:14 #: includes/subscriptions/templates/html-woo-payments-deactivate-warning.php:14 msgid "Are you sure?" msgstr "" #: includes/subscriptions/templates/html-subscriptions-plugin-notice.php:24 #. Translators: %1-%4 placeholders are opening and closing a or strong HTML #. tags. %5$s: WooPayments, %6$s: Woo Subscriptions. msgid "" "Your store has subscriptions using %5$s Stripe Billing functionality for " "payment processing. Due to the %1$soff-site billing engine%2$s these " "subscriptions use,%3$s they will continue to renew even after you " "deactivate %6$s%4$s." msgstr "" #: includes/subscriptions/templates/html-subscriptions-plugin-notice.php:38 #: includes/subscriptions/templates/html-woo-payments-deactivate-warning.php:38 #. translators: $1 $2 placeholders are opening and closing HTML link tags, #. linking to documentation. $3 is WooPayments. msgid "" "If you do not want these subscriptions to continue to be billed, you should " "%1$scancel these subscriptions%2$s prior to deactivating %3$s." msgstr "" #: includes/subscriptions/templates/html-subscriptions-plugin-notice.php:49 #: includes/subscriptions/templates/html-wcpay-deactivate-warning.php:49 #: includes/subscriptions/templates/html-woo-payments-deactivate-warning.php:49 #. translators: Placeholder is "Woo Subscriptions"". #. translators: %s: WooPayments. #. translators: Placeholder is "Woo Subscriptions"". msgid "Are you sure you want to deactivate %s?" msgstr "" #: includes/subscriptions/templates/html-subscriptions-plugin-notice.php:57 #: includes/subscriptions/templates/html-wcpay-deactivate-warning.php:57 #: includes/subscriptions/templates/html-woo-payments-deactivate-warning.php:57 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/components/cancel-authorization-button/index.tsx:20 #: client/connect-account-page/modal/index.js:89 #: client/deposits/instant-payouts/modal.tsx:41 #: client/disputes/new-evidence/index.tsx:690 #: client/onboarding/steps/embedded-kyc.tsx:71 #: client/onboarding/strings.tsx:85 #: client/order/order-status-change-strategies/index.tsx:85 #: client/order/refund-confirm-modal/index.js:107 #: client/overview/modal/reset-account/strings.tsx:28 #: client/overview/modal/update-business-details/strings.tsx:14 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:194 #: client/payment-details/summary/refund-modal/index.tsx:42 #: client/settings/general-settings/test-mode-confirm-modal.tsx:14 #: client/settings/payment-methods-list/activation-modal.tsx:26 #: client/settings/payment-methods-list/delete-modal.tsx:19 #: client/settings/transactions/manual-capture-control.tsx:55 #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/delete-button.js:77 #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/modal.js:149 msgid "Cancel" msgstr "" #: includes/subscriptions/templates/html-subscriptions-plugin-notice.php:62 #: includes/subscriptions/templates/html-wcpay-deactivate-warning.php:61 #: includes/subscriptions/templates/html-woo-payments-deactivate-warning.php:62 #. translators: Placeholder is "Woo Subscriptions"". #. translators: %s: WooPayments #. translators: Placeholder is "Woo Subscriptions"". msgid "Yes, deactivate %s" msgstr "" #: includes/subscriptions/templates/html-wcpay-deactivate-warning.php:24 #. translators: $1 $2 $3 placeholders are opening and closing HTML link tags, #. linking to documentation. $4 $5 placeholders are opening and closing strong #. HTML tags. $6 is WooPayments. msgid "" "Your store has active subscriptions using the built-in %6$s functionality. " "Due to the %1$soff-site billing engine%3$s these subscriptions use, " "%4$sthey will continue to renew even after you deactivate %6$s%5$s. " "%2$sLearn more%3$s." msgstr "" #: includes/subscriptions/templates/html-wcpay-deactivate-warning.php:38 #. translators: $1 $2 placeholders are opening and closing HTML link tags, #. linking to documentation. $3 is WooPayments. msgid "" "If you do not want these subscriptions to continue to be billed, you should " "%1$scancel all subscriptions%2$s prior to deactivating %3$s. " msgstr "" #: includes/subscriptions/templates/html-woo-payments-deactivate-warning.php:24 #. Translators: placeholders are opening and closing strong HTML tags. %6$s: #. WooPayments, %7$s: Woo Subscriptions. msgid "" "Your store has subscriptions using %6$s Stripe Billing functionality for " "payment processing. Due to the %1$soff-site billing engine%3$s these " "subscriptions use,%4$s they will continue to renew even after you " "deactivate %6$s%5$s." msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:245 #: includes/wc-payment-api/class-wc-payments-api-client.php:566 #: includes/wc-payment-api/class-wc-payments-api-client.php:645 #: includes/wc-payment-api/class-wc-payments-api-client.php:675 #: includes/wc-payment-api/class-wc-payments-api-client.php:728 #: includes/wc-payment-api/class-wc-payments-api-client.php:854 #: includes/wc-payment-api/class-wc-payments-api-client.php:879 #: includes/wc-payment-api/class-wc-payments-api-client.php:912 #: includes/wc-payment-api/class-wc-payments-api-client.php:1317 #: includes/wc-payment-api/class-wc-payments-api-client.php:1385 #: includes/wc-payment-api/class-wc-payments-api-client.php:1417 #: includes/wc-payment-api/class-wc-payments-api-client.php:1442 #: includes/wc-payment-api/class-wc-payments-api-client.php:1469 #: includes/wc-payment-api/class-wc-payments-api-client.php:1494 #: includes/wc-payment-api/class-wc-payments-api-client.php:1519 #: includes/wc-payment-api/class-wc-payments-api-client.php:1543 #: includes/wc-payment-api/class-wc-payments-api-client.php:1568 #: includes/wc-payment-api/class-wc-payments-api-client.php:1593 #: includes/wc-payment-api/class-wc-payments-api-client.php:1636 #: includes/wc-payment-api/class-wc-payments-api-client.php:1661 #: includes/wc-payment-api/class-wc-payments-api-client.php:1687 #: includes/wc-payment-api/class-wc-payments-api-client.php:1712 #: includes/wc-payment-api/class-wc-payments-api-client.php:1738 #: includes/wc-payment-api/class-wc-payments-api-client.php:1765 #: includes/wc-payment-api/class-wc-payments-api-client.php:1794 #: includes/wc-payment-api/class-wc-payments-api-client.php:1973 #: includes/wc-payment-api/class-wc-payments-api-client.php:2005 #: includes/wc-payment-api/class-wc-payments-api-client.php:2049 #: includes/wc-payment-api/class-wc-payments-api-client.php:2135 #: includes/wc-payment-api/class-wc-payments-api-client.php:2421 #: includes/wc-payment-api/class-wc-payments-api-client.php:2455 msgid "Route param validation failed." msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:538 #: includes/wc-payment-api/class-wc-payments-http.php:121 #. translators: %1: original error message. msgid "Http request failed. Reason: %1$s" msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:811 msgid "Max file size exceeded." msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:980 msgid "Currency From parameter is required" msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:1221 msgid "Onboarding field data could not be retrieved" msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:1265 msgid "Link type is required" msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:1309 msgid "Customer ID is required" msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:1377 msgid "Product ID is required" msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:1409 msgid "Price ID is required" msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:1930 msgid "Address country and line 1 are required." msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:2579 msgid "Unable to encode body for request to WooCommerce Payments API." msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:2696 msgid "Unable to decode response from WooCommerce Payments API" msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:2727 #. translators: This is an error API response. msgid "" "Error: The payment could not be captured because the requested capture " "amount is greater than the amount you can capture for this charge." msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:2764 msgid "Server error. Please try again." msgstr "" #: includes/wc-payment-api/class-wc-payments-http.php:62 msgid "Site is not connected to WordPress.com" msgstr "" #: includes/woopay/class-woopay-order-status-sync.php:65 msgid "WooPayments woopay order status sync" msgstr "" #: includes/woopay/class-woopay-session.php:112 #: includes/woopay/class-woopay-session.php:113 msgid "WooPay request is not signed correctly." msgstr "" #: includes/woopay/class-woopay-session.php:686 #: includes/woopay/class-woopay-session.php:755 msgid "Could not determine the blog ID." msgstr "" #: includes/woopay/class-woopay-session.php:888 #: includes/woopay/class-woopay-session.php:889 #: dist/woopay.js:1 #: client/components/woopay/save-user/agreement.js:29 msgid "Terms of Service" msgstr "" #: includes/woopay/class-woopay-session.php:891 #: includes/woopay/class-woopay-session.php:892 #: includes/woopay/class-woopay-session.php:1021 #: dist/woopay.js:1 #: client/components/woopay/save-user/agreement.js:43 msgid "Privacy Policy" msgstr "" #: includes/woopay/class-woopay-session.php:1029 msgid "Terms and Conditions" msgstr "" #: includes/woopay/class-woopay-session.php:1034 #. translators: %1$s terms page link, %2$s privacy page link. msgid "You must accept our %1$s and %2$s to continue with your purchase." msgstr "" #: includes/woopay/class-woopay-session.php:1042 #. translators: %1$s terms page link, %2$s privacy page link. msgid "By proceeding with your purchase you agree to our %1$s and %2$s" msgstr "" #: includes/woopay/class-woopay-session.php:1131 #. translators: %s: error message msgid "WooPay checkout encountered a fatal error: %s" msgstr "" #: includes/woopay/class-woopay-utilities.php:279 msgid "WooPay blog_token is currently misconfigured." msgstr "" #: src/Internal/Service/Level3Service.php:192 msgid "Rounding fix" msgstr "" #: src/Internal/Service/OrderService.php:270 #. Translators: %d is the ID of an order. msgid "The requested order (ID %d) was not found." msgstr "" #: templates/emails/customer-ipp-receipt.php:29 #: templates/emails/plain/customer-ipp-receipt.php:24 #. translators: %s: Customer first name msgid "Hi %s," msgstr "" #: templates/emails/customer-ipp-receipt.php:31 #: templates/emails/plain/customer-ipp-receipt.php:26 #. translators: %s: Order number msgid "This is the receipt for your order #%s:" msgstr "" #: templates/emails/email-ipp-receipt-compliance-details.php:26 #: templates/emails/plain/email-ipp-receipt-compliance-details.php:19 #: dist/index.js:1 #: client/transactions/list/index.tsx:171 msgid "Payment Method" msgstr "" #: templates/emails/email-ipp-receipt-compliance-details.php:34 #: templates/emails/plain/email-ipp-receipt-compliance-details.php:21 msgid "Application Name" msgstr "" #: templates/emails/email-ipp-receipt-store-details.php:22 msgid "Store Details" msgstr "" #: templates/emails/email-ipp-receipt-store-details.php:28 msgid "Store Name:" msgstr "" #: templates/emails/email-ipp-receipt-store-details.php:37 msgid "Address:" msgstr "" #: templates/emails/email-ipp-receipt-store-details.php:74 msgid "Contact:" msgstr "" #: templates/emails/email-ipp-receipt-store-details.php:92 msgid "Date:" msgstr "" #: woocommerce-payments.php:334 #. translators: %1 WooPayments. msgid "" "The version of Jetpack installed is too old to be used with %1$s. %1$s has " "been disabled. Please deactivate or update Jetpack." msgstr "" #: woocommerce-payments.php:350 msgid "Safe Mode" msgstr "" #: woocommerce-payments.php:351 woocommerce-payments.php:371 msgid "Safe Mode activated" msgstr "" #: woocommerce-payments.php:354 #. translators: %s: WooPayments. msgid "" "We’ve detected that you have duplicate sites connected to %s. When Safe " "Mode is active, payments will not be interrupted. However, some features " "may not be available until you’ve resolved this issue below. Safe Mode is " "most frequently activated when you’re transferring your site from one " "domain to another, or creating a staging site for testing. " "<safeModeLink>Learn more</safeModeLink>" msgstr "" #: woocommerce-payments.php:359 #. translators: %s: WooPayments. msgid "%s connection successfully transferred" msgstr "" #: woocommerce-payments.php:364 #. translators: %s: WooPayments. msgid "Safe Mode has been deactivated and %s is fully functional." msgstr "" #: woocommerce-payments.php:367 msgid "Transfer connection" msgstr "" #: woocommerce-payments.php:368 msgid "Transfer your connection" msgstr "" #: woocommerce-payments.php:369 woocommerce-payments.php:370 msgid "Create a new connection" msgstr "" #: woocommerce-payments.php:374 #. translators: %s: WooPayments. msgid "" "We’ve detected that you have duplicate sites connected to %s. When Safe " "Mode is active, payments will not be interrupted. However, some features " "may not be available until you’ve resolved this issue below. Safe Mode is " "most frequently activated when you’re transferring your site from one " "domain to another, or creating a staging site for testing. A site " "administrator can resolve this issue. <safeModeLink>Learn " "more</safeModeLink>" msgstr "" #: woocommerce-payments.php:380 #. translators: %s: WooPayments. msgid "%s Safe Mode" msgstr "" #: woocommerce-payments.php:383 msgid "Stay in Safe Mode" msgstr "" #: woocommerce-payments.php:386 #. translators: %s: WooPayments. msgid "" "<strong>Notice:</strong> It appears that your 'wp-config.php' file might be " "using dynamic site URL values. Dynamic site URLs could cause %s to enter " "Safe Mode. <dynamicSiteUrlSupportLink>Learn how to set a static site " "URL.</dynamicSiteUrlSupportLink>" msgstr "" #: woocommerce-payments.php:399 #. translators: %1$s: The current site domain name. %2$s: The original site #. domain name. Please keep hostname tags in your translation so that they can #. be formatted properly. %3$s: WooPayments. msgid "" "Transfer your %3$s connection from <hostname>%2$s</hostname> to this site " "<hostname>%1$s</hostname>. <hostname>%2$s</hostname> will be disconnected " "from %3$s." msgstr "" #: woocommerce-payments.php:411 #. translators: %1$s: The current site domain name. %2$s: The original site #. domain name. Please keep hostname tags in your translation so that they can #. be formatted properly. %3$s: WooPayments. msgid "" "Create a new connection to %3$s for <hostname>%1$s</hostname>. You’ll have " "to re-verify your business details to begin accepting payments. Your " "<hostname>%2$s</hostname> connection will remain as is." msgstr "" #: woocommerce-payments.php:423 #. translators: %1$s: The current site domain name. %2$s: The original site #. domain name. %3$s: WooPayments. msgid "" "<p><strong>Recommended for</strong></p><list><item>development " "sites</item><item>sites that need access to all %3$s " "features</item></list><p><strong>Please note</strong> that creating a fresh " "connection for <hostname>%1$s</hostname> would require restoring the " "connection on <hostname>%2$s</hostname> if that site is cloned back to " "production. <safeModeLink>Learn more</safeModeLink>.</p>" msgstr "" #: woocommerce-payments.php:435 #. translators: %s: WooPayments. msgid "" "<p><strong>Recommended for</strong></p><list><item>short-lived test " "sites</item><item>sites that will be cloned back to production after " "testing</item></list><p><strong>Please note</strong> that staying in Safe " "Mode will cause issues for some %s features such as dispute and refund " "updates, payment confirmations for local payment methods. " "<safeModeLink>Learn more</safeModeLink>.</p>" msgstr "" #: woocommerce-payments.php:444 #. translators: %1$s: The current site domain name. %2$s: The original site #. domain name. msgid "" "<span>Your site is in Safe Mode because <hostname>%1$s</hostname> appears " "to be a staging or development copy of <hostname>%2$s</hostname>.</span> " "Two sites that are telling WooPayments they’re the same site. " "<safeModeLink>Learn more about Safe Mode issues</safeModeLink>." msgstr "" #. Plugin URI of the plugin/theme msgid "https://woocommerce.com/payments/" msgstr "" #. Description of the plugin/theme msgid "Accept payments via credit card. Manage transactions within WordPress." msgstr "" #. Author of the plugin/theme msgid "WooCommerce" msgstr "" #. Author URI of the plugin/theme msgid "https://woocommerce.com/" msgstr "" #: includes/class-wc-payments-token-service.php:484 #. translators: 1: wallet name, 2: card brand msgctxt "Payment token with wallet" msgid "%1$s %2$s" msgstr "" #: includes/compat/subscriptions/class-wc-payments-email-failed-renewal-authentication.php:116 msgctxt "an email notification" msgid "Enable/disable" msgstr "" #: includes/compat/subscriptions/emails/failed-renewal-authentication-requested.php:22 #: includes/compat/subscriptions/emails/plain/failed-renewal-authentication-requested.php:18 #. translators: %1$s: an order number, %2$s: the customer's full name, %3$s: #. lowercase human time diff in the form returned by wcs_get_human_time_diff(), #. e.g. 'in 12 hours'. msgctxt "In admin renewal failed email" msgid "" "The automatic recurring payment for order %1$s from %2$s has failed. The " "customer was sent an email requesting authentication of payment. If the " "customer does not authenticate the payment, they will be requested by email " "again %3$s." msgstr "" #: includes/compat/subscriptions/emails/failed-renewal-authentication.php:18 #: includes/compat/subscriptions/emails/plain/failed-renewal-authentication.php:17 #. translators: %1$s: name of the blog, %2$s: link to payment re-authentication #. URL, note: no full stop due to url at the end. #. translators: %1$s: name of the blog, %2$s: link to checkout payment url, #. note: no full stop due to url at the end. msgctxt "In failed renewal authentication email" msgid "" "The automatic payment to renew your subscription with %1$s has failed. To " "reactivate the subscription, please login and authorize the renewal from " "your account page: %2$s" msgstr "" #: includes/multi-currency/Compatibility/WooCommerceProductAddOns.php:309 #. translators: %1$s custom addon price in order msgctxt "custom addon price in order" msgid " (%1$s)" msgstr "" #: includes/multi-currency/Compatibility/WooCommerceProductAddOns.php:303 #. translators: %1$s flat fee addon price in order msgctxt "flat fee addon price in order" msgid " (+ %1$s)" msgstr "" #: includes/subscriptions/class-wc-payments-subscriptions-onboarding-handler.php:262 #. translators: %1$s: <h3> tag, %2$s: </h3> tag, %3$s: <p> tag, %4$s: #. WooPayments, %5$s: <em> tag, %6$s: </em> tag, %7$s: <em> tag, %8$s: </em> #. tag, %9$s: </p> tag. msgctxt "used in admin pointer script params in javascript as type pointer content" msgid "" "%1$sChoose Subscription%2$s%3$s%4$s adds two new subscription product types " "- %5$sSimple subscription%6$s and %7$sVariable subscription%8$s.%9$s" msgstr "" #: includes/wc-payment-api/class-wc-payments-api-client.php:2769 #. translators: This is an error API response. msgctxt "API error message to throw as Exception" msgid "Error: %1$s" msgstr "" #: dist/index.js:1 #: client/components/test-mode-notice/index.tsx:18 msgid "loan" msgstr "" #: dist/index.js:1 #: client/capital/index.tsx:137 #: client/deposits/list/index.tsx:149 #: client/transactions/list/index.tsx:481 #: client/transactions/uncaptured/index.tsx:164 msgid "total" msgstr "" #: dist/index.js:1 #: client/capital/index.tsx:141 msgid "fixed fees" msgstr "" #: dist/index.js:1 #: client/capital/index.tsx:154 msgid "All loans" msgstr "" #: dist/index.js:1 #: client/capital/index.tsx:27 msgid "Disbursed" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/capital/index.tsx:35 #: client/card-readers/list/index.tsx:31 #: client/components/deposits-overview/recent-deposits-list.tsx:38 #: client/deposits/filters/config.js:133 #: client/deposits/list/index.tsx:73 #: client/disputes/filters/config.ts:121 #: client/disputes/index.tsx:88 #: client/payment-details/readers/index.js:71 #: client/transactions/blocked/columns.tsx:46 msgid "Status" msgstr "" #: dist/index.js:1 #: client/capital/index.tsx:50 #: client/components/active-loan-summary/index.tsx:82 msgid "Fixed fee" msgstr "" #: dist/index.js:1 #: client/capital/index.tsx:58 #: client/components/active-loan-summary/index.tsx:83 msgid "Withhold rate" msgstr "" #: dist/index.js:1 #: client/capital/index.tsx:66 #: client/components/active-loan-summary/index.tsx:86 msgid "First paydown" msgstr "" #: dist/index.js:1 #: client/capital/index.tsx:76 #. %s: date when the loan was paid off msgid "Paid off: %s" msgstr "" #: dist/index.js:1 #: client/capital/index.tsx:77 #: client/card-readers/list/list-item.tsx:12 msgid "Active" msgstr "" #: dist/index.js:1 #: client/card-readers/index.tsx:19 msgid "Connected readers" msgstr "" #: dist/index.js:1 #: client/card-readers/list/index.tsx:17 msgid "Connected card readers" msgstr "" #: dist/index.js:1 #: client/card-readers/list/index.tsx:20 #. %s: WooPayments msgid "" "Card readers are marked as active if they’ve processed one or more " "transactions during the current billing cycle. To connect or disconnect " "card readers, use the %s mobile application." msgstr "" #: dist/index.js:1 #: client/card-readers/list/index.tsx:29 msgid "Reader ID" msgstr "" #: dist/index.js:1 #: client/card-readers/list/index.tsx:30 msgid "Model" msgstr "" #: dist/index.js:1 #: client/card-readers/list/list-item.tsx:13 msgid "Inactive" msgstr "" #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: client/checkout/blocks/payment-methods-logos/logo-popover.tsx:77 msgid "Supported Credit Card Brands" msgstr "" #: dist/blocks-checkout.js:1 #: client/checkout/blocks/payment-processor.js:100 msgid "This payment method cannot be saved for future use." msgstr "" #: dist/checkout.js:1 #: dist/blocks-checkout.js:1 #: client/checkout/utils/copy-test-number.js:24 msgid "Copy the test number:" msgstr "" #: dist/checkout.js:1 #: dist/blocks-checkout.js:1 #: client/checkout/utils/copy-test-number.js:34 msgid "Test number copied to your clipboard!" msgstr "" #: dist/cart.js:1 #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/woopay-direct-checkout.js:1 #: dist/settings.js:1 #: client/checkout/woopay/connect/woopay-connect-iframe.js:89 msgid "WooPay Connect Direct Checkout" msgstr "" #: dist/checkout.js:1 #: dist/blocks-checkout.js:1 #: client/checkout/woopay/email-input-iframe.js:234 msgid "" "WooPay is unavailable at this time. Please complete your checkout below. " "Sorry for the inconvenience." msgstr "" #: dist/checkout.js:1 #: dist/blocks-checkout.js:1 #: client/checkout/woopay/email-input-iframe.js:369 msgid "Request for signature failed." msgstr "" #: dist/checkout.js:1 #: dist/blocks-checkout.js:1 #: client/checkout/woopay/email-input-iframe.js:381 msgid "Signature not found." msgstr "" #: dist/checkout.js:1 #: dist/blocks-checkout.js:1 #: dist/woopay-express-button.js:1 #: dist/settings.js:1 #: client/checkout/woopay/email-input-iframe.js:53 #: client/checkout/woopay/express-button/express-checkout-iframe.js:49 msgid "WooPay SMS code verification" msgstr "" #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: client/checkout/woopay/express-button/express-checkout-iframe.js:254 msgid "WooPay is unavailable at this time. Sorry for the inconvenience." msgstr "" #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: client/checkout/woopay/express-button/use-express-checkout-product-handler.js:39 msgid "Please fill out all required fields" msgstr "" #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: client/checkout/woopay/express-button/use-express-checkout-product-handler.js:67 msgid "Please type only valid emails" msgstr "" #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: dist/express-checkout.js:1 #: client/checkout/woopay/express-button/woopay-express-checkout-button.js:132 #: client/express-checkout/index.js:274 msgid "Sorry, this product is unavailable. Please choose a different combination." msgstr "" #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: dist/express-checkout.js:1 #: client/checkout/woopay/express-button/woopay-express-checkout-button.js:139 #: client/express-checkout/index.js:281 msgid "Please select your product options before proceeding." msgstr "" #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: client/checkout/woopay/express-button/woopay-express-checkout-button.js:32 msgid "Buy with WooPay" msgstr "" #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: client/checkout/woopay/express-button/woopay-express-checkout-button.js:321 msgid "Something went wrong. Please try again." msgstr "" #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: client/checkout/woopay/express-button/woopay-express-checkout-button.js:33 msgid "Donate with WooPay" msgstr "" #: dist/woopay-express-button.js:1 #: dist/blocks-checkout.js:1 #: dist/settings.js:1 #: client/checkout/woopay/express-button/woopay-express-checkout-button.js:34 msgid "Book with WooPay" msgstr "" #: dist/blocks-checkout.js:1 #: client/checkout/woopay/express-button/woopay-express-checkout-payment-method.js:55 msgid "" "A one-click, high-converting, secure checkout built for Woo — themed to " "your brand." msgstr "" #: dist/index.js:1 #: client/components/account-balances/balance-tooltip.tsx:19 msgid "" "{{bold}}Total balance{{/bold}} combines both pending funds (transactions " "under processing) and available funds (ready for payout). " "{{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: client/components/account-balances/balance-tooltip.tsx:27 msgid "Total balance = Available funds + Pending funds" msgstr "" #: dist/index.js:1 #: client/components/account-balances/balance-tooltip.tsx:42 msgid "" "{{bold}}Available funds{{/bold}} have completed processing and are ready to " "be dispatched to your bank account. {{learnMoreLink}}Learn " "more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: client/components/account-balances/balance-tooltip.tsx:52 msgid "" "Negative account balance? {{discoverWhyLink}}Discover " "why.{{/discoverWhyLink}}" msgstr "" #: dist/index.js:1 #: client/components/account-balances/index.tsx:104 msgid "" "Instant payouts are currently unavailable for your account. " "{{learnMoreLink}}Learn about eligibility requirements{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: client/components/account-balances/index.tsx:74 msgid "Balance" msgstr "" #: dist/index.js:1 #: client/components/account-balances/index.tsx:80 msgid "" "Get %1$s via instant payout. Funds are typically in your bank account " "within 30 mins. Fee: %2$s%%." msgstr "" #: dist/index.js:1 #: client/components/account-balances/index.tsx:87 msgid "Learn more about instant payouts" msgstr "" #: dist/index.js:1 #: client/components/account-balances/index.tsx:90 msgid "" "With {{strong}}instant payout{{/strong}} you can receive requested funds in " "your bank account within 30 mins for a %s%% fee. {{learnMoreLink}}Learn " "more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: client/components/account-balances/strings.ts:6 msgid "Available funds" msgstr "" #: dist/index.js:1 #: client/components/account-balances/strings.ts:7 msgid "Total balance" msgstr "" #: dist/index.js:1 #: client/components/account-details/account-fees/expiration-description.js:23 #. %1: total payment volume until this promotion expires %2: End date of the promotion msgid "" "Discounted base fee expires after the first %1$s of total payment volume or " "on %2$s." msgstr "" #: dist/index.js:1 #: client/components/account-details/account-fees/expiration-description.js:33 #. %1: total payment volume until this promotion expires msgid "Discounted base fee expires after the first %1$s of total payment volume." msgstr "" #: dist/index.js:1 #: client/components/account-details/account-fees/expiration-description.js:42 #. %1: End date of the promotion msgid "Discounted base fee expires on %1$s." msgstr "" #: dist/index.js:1 #: client/components/account-details/account-fees/index.js:66 msgid "Active discounts" msgstr "" #: dist/index.js:1 #: client/components/account-details/account-tools/strings.tsx:11 msgid "Account Tools" msgstr "" #: dist/index.js:1 #: client/components/account-details/account-tools/strings.tsx:13 msgid "" "You are using a test account. If you are experiencing problems completing " "account setup, or wish to test with a different email/country associated " "with your account, you can reset your account and start from the beginning." msgstr "" #: dist/index.js:1 #: client/components/account-details/account-tools/strings.tsx:14 msgid "" "Payments and payouts are disabled until account setup is completed. If you " "are experiencing problems completing account setup, or need to change the " "email/country associated with your account, you can reset your account and " "start from the beginning." msgstr "" #: dist/index.js:1 #: client/components/account-details/account-tools/strings.tsx:15 #: client/connect-account-page/strings.tsx:19 #: client/overview/modal/reset-account/strings.tsx:13 msgid "Reset account" msgstr "" #: dist/index.js:1 #: client/components/account-details/header-title.tsx:16 #: client/components/account-details/index.tsx:24 msgid "Account details" msgstr "" #: dist/index.js:1 #: client/components/account-details/header-title.tsx:23 msgid "Edit details" msgstr "" #: dist/index.js:1 #: client/components/account-details/index.tsx:25 msgid "Error loading account details." msgstr "" #: dist/index.js:1 #: client/components/account-details/payout-status-wrapper.tsx:19 msgid "More information about payout status" msgstr "" #: dist/index.js:1 #: client/components/account-details/payout-status-wrapper.tsx:28 msgid "Payouts:" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:27 msgid "Total repaid placeholder" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:28 msgid "Repaid this period" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:29 msgid "Repaid this period placeholder" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:59 msgid "Active loan overview" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:66 msgid "View transactions" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:69 msgid "Total repaid" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:69 msgid "<big>%s</big> of %s" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:72 msgid "Repaid this period (until %s)" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:72 msgid "<big>%s</big> of %s minimum" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:80 msgid "Loan disbursed" msgstr "" #: dist/index.js:1 #: client/components/active-loan-summary/index.tsx:81 msgid "Loan amount" msgstr "" #: dist/index.js:1 #: client/components/banner-notice/index.tsx:58 msgid "Dismiss this notice" msgstr "" #: dist/index.js:1 #: client/components/capture-authorization-button/index.tsx:20 msgid "Capture" msgstr "" #: dist/index.js:1 #: client/components/custom-select-control/index.tsx:62 #: client/components/inline-label-select/index.tsx:73 msgid "No selection" msgstr "" #: dist/index.js:1 #: client/components/custom-select-control/index.tsx:65 #: client/components/inline-label-select/index.tsx:76 #. %s: The selected option. msgid "Currently selected: %s" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/deposits-overview/deposit-notices.tsx:21 msgid "" "Your payouts are {{strong}}temporarily suspended{{/strong}}. " "{{suspendLink}}Learn more{{/suspendLink}}" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/deposits-overview/deposit-notices.tsx:32 #: client/settings/deposits/index.js:191 msgid "" "Payout scheduling becomes available after the standard 7-day waiting period " "for new accounts is complete. {{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/deposits-overview/deposit-notices.tsx:46 #. %s: WooPayments msgid "" "Payouts may be interrupted while your %s balance remains negative. " "{{whyLink}}Why?{{/whyLink}}" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/deposits-overview/deposit-notices.tsx:61 #. %s: a formatted currency amount, e.g. $5.00 USD msgid "" "Payouts are paused while your available funds balance remains below %s. " "{{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/deposits-overview/deposit-notices.tsx:74 msgid "You have no funds available. {{whyLink}}Why?{{/whyLink}}" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/deposits-overview/deposit-notices.tsx:93 msgid "" "Payouts are currently paused because a recent payout failed. Please " "{{updateLink}}update your bank account details{{/updateLink}}." msgstr "" #: dist/index.js:1 #: client/components/deposits-overview/deposit-schedule.tsx:25 msgid "Available funds are automatically dispatched {{strong}}every day{{/strong}}." msgstr "" #: dist/index.js:1 #: client/components/deposits-overview/deposit-schedule.tsx:39 msgid "" "Available funds are automatically dispatched {{strong}}every " "%1$s{{/strong}}." msgstr "" #: dist/index.js:1 #: client/components/deposits-overview/deposit-schedule.tsx:51 msgid "" "Available funds are automatically dispatched {{strong}}on the last day of " "every month{{/strong}}." msgstr "" #: dist/index.js:1 #: client/components/deposits-overview/deposit-schedule.tsx:60 msgid "" "Available funds are automatically dispatched {{strong}}on the %1$s of every " "month{{/strong}}." msgstr "" #: dist/index.js:1 #: client/components/deposits-overview/deposit-schedule.tsx:83 msgid "" "The timing and amount of your payouts may vary due to several factors. " "Check out our {{link}}payout schedule guide{{/link}} for details." msgstr "" #: dist/index.js:1 #: client/components/deposits-overview/index.tsx:87 #: client/deposits/list/index.tsx:199 msgid "Payout history" msgstr "" #: dist/index.js:1 #: client/components/deposits-overview/index.tsx:90 msgid "View full payout history" msgstr "" #: dist/index.js:1 #: client/components/deposits-overview/index.tsx:95 msgid "Change payout schedule" msgstr "" #: dist/index.js:1 #: client/components/deposits-overview/recent-deposits-list.tsx:37 msgid "Dispatch date" msgstr "" #: dist/index.js:1 #: client/components/dispute-status-chip/index.tsx:21 #: client/components/payment-status-chip/mappings.ts:17 msgid "Disputed: %s" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/components/dispute-status-chip/mappings.ts:13 #: client/disputes/strings.ts:180 msgid "Inquiry: Under review" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/components/dispute-status-chip/mappings.ts:17 #: client/disputes/strings.ts:181 msgid "Inquiry: Closed" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/components/dispute-status-chip/mappings.ts:21 #: client/disputes/strings.ts:182 msgid "Response needed" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/components/dispute-status-chip/mappings.ts:25 #: client/disputes/strings.ts:183 msgid "Under review" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/components/dispute-status-chip/mappings.ts:29 #: client/disputes/strings.ts:184 msgid "Charge refunded" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/components/dispute-status-chip/mappings.ts:33 #: client/disputes/strings.ts:185 msgid "Won" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/components/dispute-status-chip/mappings.ts:37 #: client/disputes/strings.ts:186 msgid "Lost" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/components/dispute-status-chip/mappings.ts:9 #: client/disputes/strings.ts:179 msgid "Inquiry: Response needed" msgstr "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:121 msgid "Please resolve the inquiry on this order of %1$s labeled '%2$s' by %3$s." msgstr "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:126 msgid "Please resolve the dispute on this order of %1$s labeled '%2$s' by %3$s." msgstr "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:141 #. %s is the number of days left to respond to the dispute. msgid "(%s day left)" msgid_plural "(%s days left)" msgstr[0] "" msgstr[1] "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:150 msgid "(Last day today)" msgstr "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:167 msgid "Please resolve the inquiry on this order of %1$s with reason '%2$s'." msgstr "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:172 msgid "This order has a payment dispute for %1$s for the reason '%2$s'. " msgstr "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:186 #. %1$s is the dispute due date. msgid "Please respond before %1$s." msgstr "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:217 msgid "Respond today" msgstr "" #: dist/order.js:1 #: dist/index.js:1 #: client/components/disputed-order-notice/index.js:218 #: client/overview/task-list/tasks/dispute-task.tsx:74 msgid "Respond now" msgstr "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:267 msgid " <a>View details</a>" msgstr "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:49 msgid "" "This order has an active payment dispute. Refunds and order editing are " "disabled." msgstr "" #: dist/order.js:1 #: client/components/disputed-order-notice/index.js:68 #: client/order/index.js:38 msgid "Refunds and order editing have been disabled as a result of a lost dispute." msgstr "" #: dist/index.js:1 #: client/components/download-button/index.tsx:16 msgid "Export" msgstr "" #: dist/settings.js:1 #: client/components/duplicate-notice/index.tsx:44 msgid "" "This payment method is enabled by other extensions. " "{{reviewExtensions}}Review extensions{{/reviewExtensions}} to improve the " "shopper experience." msgstr "" #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/settings.js:1 #: client/components/error-boundary/index.tsx:30 msgid "" "There was an error rendering this view. Please contact support for " "assistance if the problem persists." msgstr "" #: dist/settings.js:1 #: dist/index.js:1 #: client/components/file-upload/index.tsx:40 #: client/disputes/new-evidence/file-upload-control.tsx:41 msgid "Upload file" msgstr "" #: dist/settings.js:1 #: dist/index.js:1 #: client/components/file-upload/index.tsx:42 #: client/disputes/new-evidence/file-upload-control.tsx:26 #: client/settings/express-checkout-settings/file-upload.tsx:101 msgid "Remove file" msgstr "" #: dist/index.js:1 #: client/components/grouped-select-control/index.tsx:104 msgid "Search…" msgstr "" #: dist/index.js:1 #: client/components/jetpack-idc-notice/index.tsx:14 msgid "Your site is currently in Safe Mode." msgstr "" #: dist/index.js:1 #: client/components/jetpack-idc-notice/index.tsx:16 msgid "Please take action" msgstr "" #: dist/index.js:1 #: client/components/payment-status-chip/mappings.ts:24 #: client/payment-details/summary/index.tsx:283 #: client/payment-details/timeline/map-events.js:872 msgid "Partial refund" msgstr "" #: dist/index.js:1 #: client/components/payment-status-chip/mappings.ts:27 #: client/payment-details/summary/index.tsx:211 #: client/payment-details/timeline/map-events.js:871 msgid "Refunded" msgstr "" #: dist/index.js:1 #: client/components/payment-status-chip/mappings.ts:30 #: client/payment-details/timeline/map-events.js:837 msgid "Paid" msgstr "" #: dist/index.js:1 #: client/components/payment-status-chip/mappings.ts:33 msgid "Payment authorized" msgstr "" #: dist/index.js:1 #: client/components/payment-status-chip/mappings.ts:36 #: client/transactions/strings.ts:13 msgid "Refund failure" msgstr "" #: dist/index.js:1 #: client/components/payment-status-chip/mappings.ts:39 msgid "Payment failed" msgstr "" #: dist/index.js:1 #: client/components/payment-status-chip/mappings.ts:45 #: client/components/transaction-status-pill/mappings.ts:13 msgid "Needs review" msgstr "" #: dist/index.js:1 #: client/components/payment-status-chip/mappings.ts:48 #: client/components/transaction-status-pill/mappings.ts:17 msgid "Payment blocked" msgstr "" #: dist/settings.js:1 #: client/components/promotional-badge/index.tsx:12 msgid "More information" msgstr "" #: dist/index.js:1 #: client/components/risk-level/strings.ts:11 #: client/transactions/list/index.tsx:338 msgid "N/A" msgstr "" #: dist/index.js:1 #: client/components/risk-level/strings.ts:9 #: client/transactions/strings.ts:36 msgid "Highest" msgstr "" #: dist/index.js:1 #: client/components/sandbox-mode-switch-to-live-notice/index.tsx:100 #. %1$s: WooPayments msgid "" "{{div}}{{strong}}You're using a sandbox test account.{{/strong}} To accept " "real payments from shoppers, you will need to first " "{{resetAccountLink}}reset your account{{/resetAccountLink}} and, then, " "provide additional details about your business.{{/div}}{{learnMoreIcon/}}" msgstr "" #: dist/index.js:1 #: client/components/sandbox-mode-switch-to-live-notice/index.tsx:109 msgid "Learn more about sandbox accounts" msgstr "" #: dist/index.js:1 #: client/components/sandbox-mode-switch-to-live-notice/index.tsx:112 #. 1: WooPayments msgid "" "A sandbox account gives you access to all %1$s features while checkout " "transactions are simulated. {{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: client/components/sandbox-mode-switch-to-live-notice/index.tsx:133 #. %1$s: WooPayments msgid "" "{{div}}{{strong}}You are using a sandbox test account.{{/strong}} ⚠️ " "Development mode is enabled for the store! There can be no live onboarding " "process while using development, testing, or staging WordPress " "environments!{{/div}}{{learnMoreIcon/}}" msgstr "" #: dist/index.js:1 #: client/components/sandbox-mode-switch-to-live-notice/index.tsx:139 msgid "Learn more about development mode" msgstr "" #: dist/index.js:1 #: client/components/sandbox-mode-switch-to-live-notice/index.tsx:142 #. 1: WooPayments msgid "" "To begin accepting real payments, please go to the live store or change " "your {{wpEnvLink}}WordPress environment{{/wpEnvLink}} to a production one. " "{{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: client/components/sandbox-mode-switch-to-live-notice/index.tsx:36 #. %1$s: WooPayments msgid "" "{{div}}{{strong}}You're using a test account.{{/strong}} To accept payments " "from shoppers, {{switchToLiveLink}}activate your %1$s " "account.{{/switchToLiveLink}}{{/div}}{{learnMoreIcon/}}" msgstr "" #: dist/index.js:1 #: client/components/sandbox-mode-switch-to-live-notice/index.tsx:42 msgid "Learn more about test accounts" msgstr "" #: dist/index.js:1 #: client/components/sandbox-mode-switch-to-live-notice/index.tsx:45 #. 1: WooPayments msgid "" "A test account gives you access to all %1$s features while checkout " "transactions are simulated. {{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: client/components/sandbox-mode-switch-to-live-notice/index.tsx:67 #. %1$s: WooPayments msgid "" "{{div}}{{strong}}You're using a test account.{{/strong}} ⚠️ Development " "mode is enabled for the store! There can be no live onboarding process " "while using development, testing, or staging WordPress " "environments!{{/div}}{{learnMoreIcon/}}" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/sandbox-mode-switch-to-live-notice/modal/index.tsx:40 msgid "Activate payments on your store" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/sandbox-mode-switch-to-live-notice/modal/index.tsx:43 msgid "Before continuing, please make sure that you're aware of the following:" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/sandbox-mode-switch-to-live-notice/modal/index.tsx:47 msgid "" "Your test account will be deactivated, but your transactions can be found " "in your order history." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/sandbox-mode-switch-to-live-notice/modal/index.tsx:53 #. %s: WooPayments msgid "To use %s, you will need to verify your business details." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/components/sandbox-mode-switch-to-live-notice/modal/index.tsx:57 msgid "In order to receive payouts, you will need to provide your bank details." msgstr "" #: dist/wc-payments-settings-spotlight.js:1 #: dist/index.js:1 #: dist/settings.js:1 #: client/components/spotlight/index.tsx:155 #: client/merchant-feedback-prompt/negative-modal.tsx:52 #: client/merchant-feedback-prompt/positive-modal.tsx:53 msgid "Close" msgstr "" #: dist/wc-payments-settings-spotlight.js:1 #: dist/index.js:1 #: dist/settings.js:1 #: client/components/spotlight/index.tsx:73 #. %s: heading text of the spotlight dialog msgid "Dialog opened: %s" msgstr "" #: dist/index.js:1 #: client/documents/list/index.tsx:138 msgid "document" msgid_plural "documents" msgstr[0] "" msgstr[1] "" #: dist/index.js:1 #: client/deposits/list/index.tsx:143 msgid "payout" msgid_plural "payouts" msgstr[0] "" msgstr[1] "" #: dist/index.js:1 #: client/disputes/index.tsx:325 msgid "dispute" msgid_plural "disputes" msgstr[0] "" msgstr[1] "" #: dist/index.js:1 #: client/components/test-mode-notice/index.tsx:20 msgid "order" msgstr "" #: dist/index.js:1 #: client/components/test-mode-notice/index.tsx:26 msgid "created" msgstr "" #: dist/index.js:1 #: client/components/test-mode-notice/index.tsx:28 msgid "placed" msgstr "" #: dist/index.js:1 #: client/components/test-mode-notice/index.tsx:36 #. %1$s: WooPayments msgid "" "{{strong}}%1$s is in sandbox mode.{{/strong}} You need to set up a live " "%1$s account before you can accept real transactions." msgstr "" #: dist/index.js:1 #: client/components/test-mode-notice/index.tsx:43 #. %1$s: WooPayments msgid "" "{{strong}}%1$s is in test mode.{{/strong}} All transactions will be " "simulated. {{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: client/components/test-mode-notice/index.tsx:61 #. %1$s: WooPayments msgid "" "%1$s was in test mode when this %2$s was %3$s. To view live %2$ss, disable " "test mode in {{settingsLink}}%1$s settings{{/settingsLink}}." msgid_plural "" "%1$s was in test mode when these %2$ss were %3$s. To view live %2$ss, " "disable test mode in {{settingsLink}}%1$s settings{{/settingsLink}}." msgstr[0] "" msgstr[1] "" #: dist/index.js:1 #: client/components/test-mode-notice/index.tsx:71 #. %1$s: WooPayments msgid "" "Viewing test %1$s. To view live %1s, disable test mode in " "{{settingsLink}}%2s settings{{/settingsLink}}." msgstr "" #: dist/index.js:1 #: client/components/transaction-status-pill/mappings.ts:9 msgid "Succeeded" msgstr "" #: dist/index.js:1 #: client/components/welcome/currency-select.tsx:65 #: client/disputes/index.tsx:81 msgid "Currency" msgstr "" #: dist/index.js:1 #: client/components/welcome/index.tsx:17 msgid "Good morning, %s" msgstr "" #: dist/index.js:1 #: client/components/welcome/index.tsx:19 msgid "Good afternoon, %s" msgstr "" #: dist/index.js:1 #: client/components/welcome/index.tsx:21 msgid "Good evening, %s" msgstr "" #: dist/index.js:1 #: client/components/welcome/index.tsx:24 msgid "Good morning" msgstr "" #: dist/index.js:1 #: client/components/welcome/index.tsx:25 msgid "Good afternoon" msgstr "" #: dist/index.js:1 #: client/components/welcome/index.tsx:26 msgid "Good evening" msgstr "" #: dist/woopay.js:1 #: client/components/woopay/save-user/additional-information.js:9 msgid "" "Next time you buy here and on other Woo-powered stores, we'll send you a " "code to securely purchase with WooPay." msgstr "" #: dist/woopay.js:1 #: client/components/woopay/save-user/agreement.js:13 msgid "" "By continuing, you agree to WooPay's {{termsOfService/}} and " "{{privacyPolicy/}}." msgstr "" #: dist/woopay.js:1 #: client/components/woopay/save-user/checkout-page-save-user.js:346 msgid "Securely save my information for 1-click checkout" msgstr "" #: dist/woopay.js:1 #: client/components/woopay/save-user/checkout-page-save-user.js:400 msgid "Please enter a valid mobile phone number." msgstr "" #: dist/woopay.js:1 #: client/components/woopay/save-user/container.js:14 msgid "Save my info" msgstr "" #: dist/index.js:1 #: client/connect-account-page/index.tsx:319 msgid "An error occurred while setting up your sandbox account. Please try again!" msgstr "" #: dist/index.js:1 #: client/connect-account-page/index.tsx:48 msgid "Finishing payments setup" msgstr "" #: dist/index.js:1 #: client/connect-account-page/index.tsx:50 msgid "In just a few moments, you'll be ready to test payments on your store." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/connect-account-page/info-notice-modal.tsx:69 #: client/settings/fraud-protection/protection-levels/fp-modal/index.tsx:33 #: client/settings/fraud-protection/tour/steps.tsx:61 msgid "Got it" msgstr "" #: dist/index.js:1 #: client/connect-account-page/modal/index.js:42 #. %1$s: WooPayments msgid "" "It appears you're attempting to set up %1$s from an unsupported country. In " "order to complete the set up of %1$s, your store is required to have a " "business entity in one of the following countries: {{list /}} {{link}}Learn " "more{{/link}} about setting up business entities in foreign countries." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/connect-account-page/modal/index.js:80 #: client/onboarding/strings.tsx:83 #: client/settings/payment-methods-list/activation-modal.tsx:27 #: client/vat/form/tasks/vat-number-task.tsx:193 #: includes/multi-currency/client/setup/tasks/add-currencies-task/index.js:71 #: includes/multi-currency/client/setup/tasks/store-settings-task/index.js:178 msgid "Continue" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:100 msgid "How quickly will you confirm my identity and allow me to process payments?" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:101 msgid "" "We'll do our best to work with Stripe to confirm your identity as quickly " "as we can. Typically, we'll confirm your application within a couple of " "days." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:104 msgid "" "What information should I have at hand before I start the “Know Your " "Customer” process?" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:105 msgid "Here's a brief list of the information you'll need to finish payment signup:" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:108 msgid "Business owner info:" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:110 msgid "Legal name" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:111 msgid "Date of birth" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:112 msgid "Home address" msgstr "" #: dist/index.js:1 #: dist/woopay.js:1 #: dist/settings.js:1 #: client/connect-account-page/strings.tsx:114 #: client/settings/phone-input/index.tsx:140 msgid "Mobile phone number" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:115 msgid "Bank account information" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:116 msgid "Social Security number (SSN) or Taxpayer Identification Number" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:12 msgid "Connect your store" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:120 msgid "Business info:" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:122 msgid "Country where your business is based" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:123 msgid "Type of business" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:124 msgid "Industry" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:125 msgid "Company address" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:126 msgid "Company phone number" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:127 msgid "Company URL" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:131 msgid "You’re only steps away from getting paid" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:133 msgid "Create and connect your account" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:134 msgid "To ensure safe and secure transactions, a WordPress.com account is required." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:137 msgid "Provide a few business details" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:138 msgid "" "Next we’ll ask you to verify your business and payment details to enable " "payouts." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:14 msgid "Verify business details" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:141 msgid "Setup complete!" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:144 #. %s: WooPayments msgid "You’re ready to start using the features and benefits of %s." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:146 msgid "" "We've temporarily paused new account creation. We'll notify you when we " "resume!" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:148 msgid "Limited time offer" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:149 msgid "" "Discount will be applied to payments processed via WooPayments upon " "completion of installation, setup, and connection." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:150 msgid "*See <a>Terms and Conditions</a> for details." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:155 msgid "" "There was an error applying the promotion. Please contact support for " "assistance if the problem persists" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:159 #. %1$s: WooPayments msgid "" "<b>%1$s is not currently available in your location</b>. To be eligible for " "%1$s, your business address must be in one of the following <a>supported " "countries</a>." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:17 msgid "Finish business details verifications" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:18 msgid "Create test account" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:23 #. %s: first name of the merchant, if it exists, %s: WooPayments. msgid "Hi%s, Welcome to %s!" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:27 msgid "Automatic - Daily" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:30 msgid "Payments capture" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:31 msgid "Capture on order" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:34 msgid "Recurring payments" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:35 msgid "Supported" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:38 msgid "" "Offer card payments, Apple Pay, iDeal, Affirm, Afterpay, and accept " "in-person payments with the Woo mobile app." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:39 msgid "" "Sell to international markets and accept over 135 currencies with local " "payment methods." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:40 msgid "Earn recurring revenue and get payouts into your bank account." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:42 msgid "I'm setting up a store for someone else." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:45 #. %s: WooPayments msgid "" "This option will set up a %s test account using test data. When you’re " "ready to launch your store, switching to live payments is easy." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:48 msgid "" "Test mode is enabled, only test accounts will be created. If you want to " "process live transactions, please {{learnMoreLink}}disable " "it{{/learnMoreLink}}." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:58 #. 1: WooPayments. msgid "Please <b>complete your %1$s setup</b> to process payments." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:61 msgid "You'll need to verify your business and payment details to " msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:62 msgid "" "To ensure safe and secure transactions, a WordPress.com account is required " "before you can verify your business details." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:64 msgid "enable payouts." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:69 #. %s: WooPayments msgid "Verifying your information with %s" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:71 msgid "Why we ask for personal financial information" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:74 #. %s: WooPayments msgid "" "As you continue the process of signing up for %s, we'll ask for information " "about your business, including the business owner's date of birth and tax " "ID number. We know you may wonder why we ask for this information, and how " "it will be used. The “Know Your Customer” process, explained below, helps " "us provide a safe, ethical environment for all financial transactions." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:77 msgid "What is “Know Your Customer”?" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:78 msgid "" "“Know Your Customer” standards are used by banks and other financial " "institutions to confirm that customers are who they say they are. By " "confirming their customers' identities, banks and financial institutions " "can help keep transactions safe from fraud and other suspicious activities." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:81 msgid "Why do I have to share this information?" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:82 msgid "" "Before we build a payment relationship with a customer, we ask for the " "information listed above to validate the business owner's identity and tax " "ID number, and to ensure that we can connect the listed bank account with " "the business itself." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:83 msgid "" "The ultimate goal of the “Know Your Customer” process is to help your " "business get up and running with payments as soon as possible while " "protecting your business and your customers. We follow the same regulations " "as other financial institutions so that we can ensure we operate in an " "ethical and trustworthy manner. We want to protect your business and the " "payments that we manage for you. The “Know Your Customer” process helps us " "protect you." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:86 msgid "What else should I keep in mind while completing this process?" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:89 #. %s: WooPayments msgid "" "If you're setting up %s for someone else, it's best to have that person " "complete the account creation process. As you can see above, we ask for " "very specific information about the business owner - and you might not have " "all the details at hand. It's not always possible to change account " "information once it's been saved, especially if the site accepts live " "transactions before the correct account information is entered." msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:94 #. %s: WooPayments msgid "Is my data safe with %s?" msgstr "" #: dist/index.js:1 #: client/connect-account-page/strings.tsx:97 #. %s: WooPayments msgid "" "We take every step required to safeguard your personal data. %s is built in " "partnership with Stripe to store your data in a safe and secure manner." msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: client/constants/payment-method.ts:47 msgid "ACH Credit Transfer" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: client/constants/payment-method.ts:48 msgid "ACH Debit" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: client/constants/payment-method.ts:49 msgid "ACSS Debit" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: dist/multi-currency.js:1 #: client/constants/payment-method.ts:51 #: client/payment-methods-icons.tsx:36 msgid "American Express" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: client/constants/payment-method.ts:52 msgid "Card Payment" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: client/constants/payment-method.ts:53 msgid "In-Person Card Payment" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: dist/multi-currency.js:1 #: client/constants/payment-method.ts:54 #: client/payment-methods-icons.tsx:37 msgid "Cartes Bancaires" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: dist/multi-currency.js:1 #: client/constants/payment-method.ts:55 #: client/payment-methods-icons.tsx:38 msgid "Diners Club" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: dist/multi-currency.js:1 #: client/constants/payment-method.ts:56 #: client/payment-methods-icons.tsx:39 msgid "Discover" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: dist/multi-currency.js:1 #: client/constants/payment-method.ts:57 #: client/payment-methods-icons.tsx:40 #: client/payment-methods-map.tsx:21 msgid "JCB" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: dist/multi-currency.js:1 #: client/constants/payment-method.ts:58 #: client/payment-methods-icons.tsx:41 msgid "Mastercard" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: client/constants/payment-method.ts:59 msgid "Stripe Account" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: client/constants/payment-method.ts:60 msgid "Union Pay" msgstr "" #: dist/index.js:1 #: dist/checkout.js:1 #: dist/settings.js:1 #: dist/multi-currency.js:1 #: client/constants/payment-method.ts:61 #: client/payment-methods-icons.tsx:43 msgid "Visa" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:106 #. %s Order id msgid "Payment for order #%s captured successfully." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:111 #. %s Order id msgid "There has been an error capturing the payment for order #%s." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:148 #. %s Order id msgid "Payment for order #%s canceled successfully." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:153 #. %s Order id msgid "There has been an error canceling the payment for order #%s." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:17 msgid "The payment amount is too small to be processed." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:23 #. %1$s: minimum amount, %2$s: currency code msgid "The minimum amount that can be processed is %1$s %2$s." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:26 msgid "The order could not be found." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:27 msgid "Payment cannot be processed for partially or fully refunded orders." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:28 msgid "The payment cannot be processed due to a mismatch with order details." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:29 msgid "This payment cannot be processed in its current state." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:36 msgid "The payment capture failed to complete." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:38 msgid "The payment cancellation failed to complete." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:39 msgid "An unexpected error occurred. Please try again later." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/actions.ts:45 msgid "Unable to process the payment. Please try again later." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/resolvers.ts:54 msgid "Error retrieving authorization." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/authorizations/resolvers.ts:70 msgid "Error retrieving uncaptured transactions." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/capital/resolvers.ts:37 msgid "Error retrieving the active loan summary." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/charges/resolvers.js:42 #: client/data/payment-intents/resolvers.ts:21 msgid "Error retrieving transaction." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/deposits/actions.js:114 msgid "Instant payout for %s in transit." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/deposits/actions.js:123 msgid "View details" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/deposits/actions.js:135 msgid "Error creating instant payout." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/deposits/resolvers.js:133 msgid "Error retrieving payouts." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/deposits/resolvers.js:48 msgid "Error retrieving payout." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/deposits/resolvers.js:66 msgid "Error retrieving all payouts' overviews." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/disputes/actions.js:75 #. #%s is an order number, e.g. 15 msgid "You have accepted the dispute for order #%s." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/disputes/actions.js:81 msgid "You have accepted the dispute." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/disputes/actions.js:88 msgid "There has been an error accepting the dispute. Please try again later." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/disputes/resolvers.js:115 msgid "Error retrieving the summary of disputes." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/disputes/resolvers.js:69 msgid "Error retrieving dispute." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/disputes/resolvers.js:96 msgid "Error retrieving disputes." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/documents/resolvers.js:57 msgid "Error retrieving documents." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/files/resolvers.ts:26 msgid "Error retrieving file." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/payment-intents/actions.ts:42 #. %s payment intent id msgid "Refunded payment #%s." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/payment-intents/actions.ts:47 #. %s payment intent id msgid "There has been an error refunding the payment #%s. Please try again later." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/pm-promotions/actions.ts:57 msgid "Promotion activated successfully!" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/pm-promotions/actions.ts:62 msgid "Error activating promotion. Please try again." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/pm-promotions/actions.ts:78 msgid "Promotion dismissed." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/pm-promotions/actions.ts:83 msgid "Error dismissing promotion. Please try again." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/pm-promotions/resolvers.ts:68 msgid "Error retrieving promotions. Please try again later." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: dist/plugins-page.js:1 #: client/data/settings/actions.js:202 msgid "Settings saved." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: dist/plugins-page.js:1 #: client/data/settings/actions.js:207 msgid "Error saving settings." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: dist/plugins-page.js:1 #: client/data/settings/actions.js:296 msgid "Error starting the Stripe Billing migration." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: dist/plugins-page.js:1 #: client/data/settings/actions.js:315 #: includes/multi-currency/client/data/actions.js:157 msgid "Error saving option" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/settings/resolvers.js:27 msgid "Error retrieving settings." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/transactions/resolvers.js:153 msgid "Error retrieving transactions." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/data/transactions/resolvers.js:199 msgid "Error retrieving on review transactions." msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:100 msgid "Copy bank reference ID to clipboard" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:100 msgid "Not available" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:114 msgid "Payout transactions" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:47 #: client/transactions/list/index.tsx:212 msgid "Payout date" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:49 msgid "Instant payout date" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:52 msgid "Withdrawal date" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:68 msgid "Withdrawal overview" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:69 msgid "Payout overview" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:72 msgid "Withdrawal amount" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:73 msgid "Payout amount" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:76 #. %s - amount representing the fee percentage msgid "%s service fee" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:78 msgid "Net withdrawal amount" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:79 msgid "Net payout amount" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:82 msgid "Failure reason: " msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:85 msgid "Unknown" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:89 msgid "Withdrawal details" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:94 #: client/deposits/list/index.tsx:81 msgid "Bank account" msgstr "" #: dist/index.js:1 #: client/deposits/details/index.tsx:97 #: client/deposits/list/index.tsx:87 msgid "Bank reference ID" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/deposits/filters/config.js:103 #: client/disputes/filters/config.ts:97 #: client/documents/filters/config.ts:52 #: client/transactions/filters/config.ts:122 msgid "{{title}}Date{{/title}} {{rule /}} {{filter /}}" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/deposits/filters/config.js:107 #: client/disputes/filters/config.ts:98 #: client/documents/filters/config.ts:53 #: client/transactions/filters/config.ts:123 msgid "<title>Date</title> <rule /> <filter />" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:111 msgid "Select a payout date" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/deposits/filters/config.js:116 #: client/disputes/filters/config.ts:104 #: client/documents/filters/config.ts:59 #: client/transactions/filters/config.ts:129 msgid "Before" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/deposits/filters/config.js:120 #: client/disputes/filters/config.ts:108 #: client/documents/filters/config.ts:63 #: client/transactions/filters/config.ts:133 msgid "After" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/deposits/filters/config.js:124 #: client/disputes/filters/config.ts:112 #: client/documents/filters/config.ts:67 #: client/transactions/filters/config.ts:137 msgid "Between" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:134 msgid "Remove payout status filter" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:138 msgid "Select a payout status filter match" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/deposits/filters/config.js:145 #: client/disputes/filters/config.ts:126 msgid "{{title}}Status{{/title}} {{rule /}} {{filter /}}" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/deposits/filters/config.js:149 #: client/disputes/filters/config.ts:127 msgid "<title>Status</title> <rule /> <filter />" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:153 msgid "Select a payout status" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:27 msgid "Payout currency" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:45 msgid "All" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/deposits/filters/config.js:53 #: client/disputes/filters/config.ts:53 #: client/documents/filters/config.ts:19 #: client/transactions/filters/config.ts:77 msgid "Show" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:65 msgid "All payouts" msgstr "" #: dist/index.js:1 #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/deposits/filters/config.js:69 #: client/disputes/filters/config.ts:74 #: client/documents/filters/config.ts:29 #: client/transactions/filters/config.ts:94 msgid "Advanced filters" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:86 msgid "Payouts match {{select /}} filters" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:87 msgid "Payouts match <select /> filters" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/deposits/filters/config.js:91 #: client/deposits/list/index.tsx:47 #: client/documents/filters/config.ts:47 #: client/documents/list/index.tsx:25 #: client/payment-details/summary/index.tsx:110 #: client/settings/deposits/index.js:118 #: client/transactions/filters/config.ts:117 msgid "Date" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:92 msgid "Remove payout date filter" msgstr "" #: dist/index.js:1 #: client/deposits/filters/config.js:96 msgid "Select a payout date filter match" msgstr "" #: dist/index.js:1 #: client/deposits/index.tsx:75 msgid "" "Payouts are currently paused because a recent payout failed. Please " "{{updateLink/}}." msgstr "" #: dist/index.js:1 #: client/deposits/index.tsx:80 msgid "update your bank account details" msgstr "" #: dist/index.js:1 #: client/deposits/instant-payouts/index.tsx:35 msgid "Get %s now" msgstr "" #: dist/index.js:1 #: client/deposits/instant-payouts/modal.tsx:19 #. %s: amount representing the fee percentage, <a>: instant payout doc URL msgid "" "Need cash in a hurry? Instant payouts are available within 30 minutes for a " "nominal %s service fee. <a>Learn more</a>" msgstr "" #: dist/index.js:1 #: client/deposits/instant-payouts/modal.tsx:24 msgid "Instant payout" msgstr "" #: dist/index.js:1 #: client/deposits/instant-payouts/modal.tsx:28 msgid "Balance available for instant payout: " msgstr "" #: dist/index.js:1 #: client/deposits/instant-payouts/modal.tsx:33 #. %s - amount representing the fee percentage msgid "%s service fee: " msgstr "" #: dist/index.js:1 #: client/deposits/instant-payouts/modal.tsx:38 msgid "Net payout amount: " msgstr "" #: dist/index.js:1 #: client/deposits/instant-payouts/modal.tsx:44 #. %s: Monetary amount to pay out msgid "Pay out %s now" msgstr "" #: dist/index.js:1 #: client/deposits/list/index.tsx:185 msgid "" "You are about to export %d deposits. If you'd like to reduce the size of " "your export, you can use one or more filters. Would you like to continue?" msgstr "" #: dist/index.js:1 #: client/deposits/list/index.tsx:194 #: client/disputes/index.tsx:317 #: client/transactions/list/index.tsx:454 msgid "" "We’re processing your export. 🎉 The file will download automatically and " "be emailed to %s." msgstr "" #: dist/index.js:1 #: client/deposits/list/index.tsx:58 #: client/documents/filters/config.ts:76 #: client/documents/list/index.tsx:37 #: client/payment-details/payment-method/card-present/index.js:121 #: client/payment-details/payment-method/card/index.js:146 #: client/transactions/filters/config.ts:199 #: client/transactions/list/index.tsx:102 msgid "Type" msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:16 msgid "Completed (paid)" msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:17 msgid "Completed (deducted)" msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:19 msgid "In transit" msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:20 msgid "Canceled" msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:21 #: client/payment-details/payment-method/card/check.js:16 #: client/payment-details/timeline/map-events.js:921 msgid "Failed" msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:27 msgid "Your account has insufficient funds to cover your negative balance." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:28 msgid "" "The bank account has restrictions on either the type or number of transfers " "allowed. This normally indicates that the bank account is a savings or " "other non-checking account." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:29 msgid "" "Debit transactions are not approved on your bank account. Bank accounts " "need to be set up for both credit and debit transfers." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:30 msgid "" "The card used was invalid. This usually means the card number is invalid or " "the account has been closed." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:31 msgid "" "The bank has declined this transfer. Please contact the bank for more " "information." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:32 msgid "" "The transfer was refused by the issuing bank because this type of payment " "is not permitted for this card. Please contact the issuing bank for " "clarification." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:33 msgid "" "The transfer was refused by the card issuer. Please contact the issuing " "bank for clarification." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:34 msgid "The bank no longer supports transfers to this card." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:35 msgid "" "The card used has been reported lost or stolen. Please contact the issuing " "bank for clarification." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:36 msgid "" "The issuer specified by the card number does not exist. Please verify card " "details." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:37 msgid "" "The card used has expired. Please switch to a different card or payment " "method. Contact the issuing bank for clarification." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:38 msgid "The bank or the payment processor could not process this transfer." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:41 msgid "" "The bank account details on file are probably incorrect. While the routing " "number appears correct, the account number is invalid." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:42 msgid "The bank account holder name on file appears to be incorrect." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:43 msgid "The bank account has been closed." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:44 msgid "" "The bank account details on file are probably incorrect. No bank account " "could be located with those details." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:45 msgid "" "The card issuer has declined the transaction as it will exceed the card " "limit. Please switch to a different card or payment method. Contact the " "issuing bank for clarification." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:46 msgid "The bank account has been frozen." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:47 msgid "" "The issuing bank is currently unavailable. Our system will automatically " "try again on your next payout date, or you can switch to a different payout " "method." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:48 msgid "" "The bank was unable to process this transfer because of its currency. This " "is probably because the bank account cannot accept payments in that " "currency." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:49 msgid "" "The bank account type is incorrect. This value can only be checking or " "savings in most countries. In Japan, it can only be futsu or toza." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:50 msgid "" "The bank could not process this transfer. Please check that the entered " "bank account details match the corresponding account bank statement exactly." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:51 msgid "" "The destination bank account is no longer valid because its branch has " "changed ownership." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:52 msgid "" "The selected card has exceeded its card usage frequency limit. Please " "switch to a different card or payment method. Contact the issuing bank for " "clarification." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:53 msgid "" "Your bank notified us that the bank account holder address on file is " "incorrect." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:54 msgid "" "Your bank notified us that the bank account holder tax ID on file is " "incorrect." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:55 msgid "Your bank notified us that the bank account number is too long." msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:7 msgid "Payout" msgstr "" #: dist/index.js:1 #: client/deposits/strings.ts:8 msgid "Withdrawal" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/deposits/utils/index.ts:21 msgid "Last day of the month" msgstr "" #: dist/settings.js:1 #: client/disable-confirmation-modal/index.js:154 #. %s: WooPayments msgid "" "{{strong}}Need help?{{/strong}} Learn more about " "{{wooCommercePaymentsLink}}%s{{/wooCommercePaymentsLink}} or " "{{contactSupportLink}}contact WooCommerce Support{{/contactSupportLink}}." msgstr "" #: dist/settings.js:1 #: dist/tos.js:1 #: client/disable-confirmation-modal/index.js:43 #: client/tos/modal/index.tsx:50 #. %s: WooPayments msgid "Disable %s" msgstr "" #: dist/settings.js:1 #: client/disable-confirmation-modal/index.js:76 #. %s: WooPayments msgid "" "%s is currently powering multiple popular payment methods on your store. " "Without it, they will no longer be available to your customers, which may " "influence sales." msgstr "" #: dist/settings.js:1 #: client/disable-confirmation-modal/index.js:91 #. %s: WooPayments msgid "Payment methods that need %s:" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:122 msgid "Remove dispute status filter" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:123 msgid "Select a dispute status filter match" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:128 msgid "Select a dispute status" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:27 msgid "Dispute currency" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:45 #: client/transactions/filters/config.ts:69 #: includes/multi-currency/client/analytics/index.js:129 #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/modal.js:185 #: includes/multi-currency/client/setup/tasks/add-currencies-task/index.js:306 msgid "All currencies" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:66 msgid "Needs response" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:70 msgid "All disputes" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:87 msgid "Disputes match {{select /}} filters" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:88 msgid "Disputes match <select /> filters" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:92 msgid "Disputed on date" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:93 msgid "Remove dispute date filter" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:94 msgid "Select a dispute date filter match" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/filters/config.ts:99 msgid "Select a dispute date" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:102 msgid "Source" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:115 #: client/payment-details/summary/index.tsx:118 #: client/transactions/blocked/columns.tsx:39 #: client/transactions/list/index.tsx:177 msgid "Customer" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:121 #: client/transactions/list/index.tsx:183 #: client/transactions/uncaptured/index.tsx:66 msgid "Email" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:128 #: client/transactions/list/index.tsx:190 #: client/transactions/uncaptured/index.tsx:73 msgid "Country" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:135 msgid "Disputed on" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:145 msgid "Respond by" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:153 #: client/transactions/uncaptured/index.tsx:80 msgid "Action" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:184 msgid "Last day today" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:187 #. %s is the number of days left to respond to the dispute. msgid "%s day left" msgid_plural "%s days left" msgstr[0] "" msgstr[1] "" #: dist/index.js:1 #: client/disputes/index.tsx:272 msgid "Respond" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:273 msgid "See details" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:308 msgid "" "You are about to export %d disputes. If you'd like to reduce the size of " "your export, you can use one or more filters. Would you like to continue?" msgstr "" #: dist/index.js:1 #: client/disputes/index.tsx:95 #: client/disputes/new-evidence/index.tsx:400 #: client/payment-details/dispute-details/dispute-summary-row.tsx:35 msgid "Reason" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:23 msgid "Evidence submitted successfully" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:24 msgid "Thanks for sharing your response!" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:26 msgid "Your response has been submitted under Visa’s compliance process." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:27 msgid "Your evidence has been sent to the cardholder's bank for review." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:29 msgid "What’s next?" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:32 msgid "" "Visa will review your submission under its network rules and determine the " "outcome of the dispute." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:33 msgid "" "The cardholder’s bank will review your response. Please be patient — this " "usually takes a few weeks, but in some cases it can take up to 3 months." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:34 msgid "" "This review typically takes several weeks, but in some cases may take up to " "3 months." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:35 msgid "" "You'll be informed of any updates via email, or you can check the status of " "your case at any time in your <disputesPageLink>Disputes " "area</disputesPageLink>." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:39 msgid "Disputes page" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:42 msgid "Useful resources" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:44 msgid "" "Help prevent any further disputes by <learnMoreLink>following the advice in " "our guide</learnMoreLink>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:45 msgid "Learn more about preventing disputes" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:47 msgid "" "Learn more about the dispute process using <learnMoreLink>our " "resources</learnMoreLink>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:48 msgid "Learn more about disputes" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:51 #: client/disputes/new-evidence/index.tsx:584 #: client/payment-details/dispute-details/dispute-steps.tsx:93 msgid "" "<strong>The outcome of this dispute will be determined by Visa.</strong> " "WooPayments has no influence over the decision and is not liable for any " "chargebacks." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:55 #: client/disputes/new-evidence/index.tsx:580 #: client/payment-details/dispute-details/dispute-steps.tsx:194 msgid "" "<strong>The outcome of this dispute will be determined by %1$s.</strong> " "WooPayments has no influence over the decision and is not liable for any " "chargebacks." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:56 #: client/disputes/new-evidence/index.tsx:581 #: client/payment-details/dispute-details/dispute-steps.tsx:195 msgid "" "<strong>The outcome of this dispute will be determined by the cardholder's " "bank.</strong> WooPayments has no influence over the decision and is not " "liable for any chargebacks." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:65 msgid "Return to disputes" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/confirmation-screen.tsx:72 msgid "View submitted dispute" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:104 msgid "Cancellation policy" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:108 #: client/disputes/new-evidence/evidence-matrix.ts:141 #: client/disputes/new-evidence/recommended-document-fields.ts:255 msgid "Terms of service" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:113 #: client/disputes/new-evidence/evidence-matrix.ts:166 #: client/disputes/new-evidence/recommended-document-fields.ts:89 msgid "Other documents" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:143 #. %1$s: label, %2$s: attachment letter msgid "• %1$s (Attachment %2$s)" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:153 #. %s: attachment letter msgid "• <Attachment description> (Attachment %s)" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:168 #. %s: acquiring bank name msgid "To: %s" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:171 #. %s: case number msgid "Subject: Chargeback Dispute – Case #%s" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:180 #. %1$s: customer name, %2$s: product, %3$s: order date msgid "" "Our records indicate that the customer and legitimate cardholder, %1$s, " "ordered %2$s on %3$s." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:185 msgid "" "Based on this information, we respectfully request that the chargeback be " "reversed. Please let me know if any further details are required." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:196 #. %1$s: customer name, %2$s: order date, %3$s: refund amount msgid "" "Our records indicate that the customer, %1$s, was refunded on %2$s for the " "amount of %3$s. The refund was processed through our payment provider and " "should be visible on the customer's statement within 7 - 10 business days." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:209 msgid "" "The customer requested a refund outside of the eligible window outlined in " "our refund policy, which was clearly presented on the website and on the " "order confirmation." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:237 #. %1$s: customer name, %2$s: product, %3$s: order date, %4$s: delivery date msgid "" "Our records indicate that the customer, %1$s, ordered %2$s on %3$s and " "received it on %4$s." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:251 #. %1$s: customer name, %2$s: product, %3$s: order date msgid "" "Our records indicate that the customer, %1$s, ordered %2$s on %3$s. The " "product matched the description provided at the time of sale, and we did " "not receive any indication from the customer that it was defective or not " "as described." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:265 #. %1$s: customer name, %2$s: product msgid "" "Our records indicate that the customer, %1$s, subscribed to %2$s and was " "billed according to the terms accepted at the time of signup. The " "customer's account remained active and no cancellation was recorded prior " "to the billing date." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:27 msgid "<Delivery/Service Date>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:280 #. %1$s: order date, %2$s: refund amount msgid "" "Our records indicate that this charge was a duplicate of a previous " "transaction. A refund has already been issued to the customer on %1$s for " "the amount of %2$s. This refund should be visible on the customer's " "statement within 7 - 10 business days." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:282 msgid "[Refund Amount]" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:292 #. %1$s: case number, %2$s: transaction ID, %3$s: transaction date msgid "" "We are submitting evidence in response to chargeback #%1$s for transaction " "#%2$s on %3$s." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:295 #. %1$s: case number, %2$s: transaction ID msgid "" "Our records show that the customer placed two distinct orders: %1$s and " "%2$s. Both transactions were legitimate, fulfilled independently, and are " "not duplicates." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:297 msgid "To support our case, we are providing the following documentation:" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:300 msgid "" "Based on this information, we respectfully request that the chargeback be " "reversed. Please let us know if any further details are required." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:316 msgid "Thank you," msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:331 msgid "<business@email.com>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:333 msgid "<Business Phone Number>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:335 msgid "<Bank Name>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:336 msgid "<Case Number>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:338 msgid "<Transaction ID>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:344 msgid "<Transaction Date>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:346 msgid "<Customer Name>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:351 msgid "<Product>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:357 msgid "<Order Date>" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:368 msgid "Dear Dispute Resolution Team," msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:50 #: client/disputes/new-evidence/evidence-matrix.ts:129 #: client/disputes/new-evidence/recommended-document-fields.ts:77 msgid "Order receipt" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:57 #: client/disputes/new-evidence/evidence-matrix.ts:50 msgid "Any additional receipts" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:61 #: client/disputes/new-evidence/evidence-matrix.ts:23 msgid "Refund receipt" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:68 #: client/disputes/new-evidence/evidence-matrix.ts:160 msgid "Prior undisputed transaction history" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:73 #: client/disputes/new-evidence/recommended-document-fields.ts:83 msgid "Customer communication" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:77 #: client/disputes/new-evidence/recommended-document-fields.ts:229 msgid "Customer's signature" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:81 #: client/disputes/new-evidence/evidence-matrix.ts:135 #: client/disputes/new-evidence/recommended-document-fields.ts:249 msgid "Store refund policy" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:85 #: client/disputes/new-evidence/recommended-document-fields.ts:280 msgid "Proof of shipping" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:89 #: client/disputes/new-evidence/recommended-document-fields.ts:215 msgid "Item condition" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:94 msgid "Subscription logs" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter-generator.ts:99 #: client/disputes/new-evidence/evidence-matrix.ts:87 msgid "Cancellation logs" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter.tsx:18 msgid "Cover Letter" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter.tsx:76 msgid "Print Cover Letter" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter.tsx:93 msgid "COVER LETTER" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/cover-letter.tsx:94 msgid "Preview cover letter" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/customer-details.tsx:27 msgid "Customer details" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/customer-details.tsx:30 msgid "NAME" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/customer-details.tsx:33 msgid "PHONE" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/customer-details.tsx:36 msgid "EMAIL" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/customer-details.tsx:39 msgid "IP ADDRESS" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/customer-details.tsx:42 msgid "BILLING ADDRESS" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/duplicate-status.tsx:17 msgid "Was this charge a duplicate?" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/duplicate-status.tsx:23 msgid "A refund has been issued" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/duplicate-status.tsx:24 msgid "It was a duplicate" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/duplicate-status.tsx:30 msgid "No refund needed" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/duplicate-status.tsx:31 msgid "It was not a duplicate" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/evidence-matrix.ts:130 #: client/disputes/new-evidence/recommended-document-fields.ts:78 msgid "" "A copy of the customer's receipt, which can be found in the receipt history " "for this transaction." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/evidence-matrix.ts:136 #: client/disputes/new-evidence/recommended-document-fields.ts:250 msgid "A screenshot of your store's refund policy." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/evidence-matrix.ts:142 #: client/disputes/new-evidence/recommended-document-fields.ts:256 msgid "A screenshot of your store's terms of service." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/evidence-matrix.ts:161 msgid "" "Proof of past undisputed transactions from the same customer, with matching " "billing and device details." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/evidence-matrix.ts:167 #: client/disputes/new-evidence/recommended-document-fields.ts:90 msgid "Any other relevant documents that will support your case." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/evidence-matrix.ts:24 msgid "A confirmation that a refund was issued." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/evidence-matrix.ts:51 msgid "Receipt(s) for any other order(s) from this customer." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/evidence-matrix.ts:56 msgid "Refund policy" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/evidence-matrix.ts:88 msgid "" "Records showing no cancellation attempt or request was made before the " "charge, such as account activity, subscription status, or communication " "history." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:274 msgid "Evidence submitted!" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:275 msgid "Evidence saved!" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:294 msgid "Failed to submit evidence. (%s)" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:295 msgid "Failed to save evidence. (%s)" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:302 msgid "Please wait until file upload is finished" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:387 #: client/payment-details/dispute-details/dispute-summary-row.tsx:22 msgid "Dispute Amount" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:391 #: client/payment-details/dispute-details/dispute-summary-row.tsx:26 msgid "Disputed On" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:410 #: client/payment-details/dispute-details/dispute-summary-row.tsx:45 msgid "Respond By" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:438 msgid "Loading dispute…" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:485 msgid "" "The files you've attached to this dispute as evidence will exceed the limit " "for a dispute's total size. Try using smaller files as evidence. Hint: if " "you've attached images, you might want to try providing them in lower " "resolutions." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:528 msgid "Failed to upload file. (%s)" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:590 msgid "Tell us about the dispute" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:591 msgid "" "This is a compliance case and the issuer has indicated network rules have " "been violated. Please check for accuracy and upload any relevant documents." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:594 msgid "" "Input any context you think the issuer and network should have while " "reviewing this dispute." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:595 msgid "Why do you disagree with this dispute?" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:595 msgid "Please enter any relevant details here." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:639 msgid "" "You've made some manual edits to your cover letter. If you update your " "evidence again, those changes won't be reflected here automatically — but " "you can always make further edits yourself." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:700 msgid "Next" msgstr "" #: dist/index.js:1 #: dist/tos.js:1 #: client/disputes/new-evidence/index.tsx:703 #: client/onboarding/strings.tsx:84 #: client/tos/modal/index.tsx:67 msgid "Back" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:705 msgid "Save for later" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:708 msgid "" "Are you sure you're ready to submit this evidence? Evidence submissions are " "final." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/index.tsx:712 msgid "Submit" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:16 msgid "Physical products" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:20 msgid "Digital products" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:24 msgid "Offline service" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:30 msgid "Booking/Reservation" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:34 #: client/payment-details/summary/refund-modal/index.tsx:64 msgid "Other" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:40 msgid "Multiple product types" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:47 msgid "Product or service details" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:48 msgid "Product details" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:50 msgid "" "Please ensure the product or service type and description have been entered " "accurately." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:51 msgid "Please ensure the product type and description have been entered accurately." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:54 msgid "PRODUCT OR SERVICE TYPE" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:55 msgid "PRODUCT TYPE" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:58 msgid "PRODUCT OR SERVICE DESCRIPTION" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/product-details.tsx:59 msgid "PRODUCT DESCRIPTION" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-document-fields.ts:162 msgid "" "Any documents showing the billing history, subscription status, or " "cancellation logs, for example." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-document-fields.ts:216 msgid "A screenshot of the item condition." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-document-fields.ts:230 msgid "" "Any relevant documents showing the customer's signature, such as signed " "proof of delivery." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-document-fields.ts:243 msgid "Proof of active subscription" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-document-fields.ts:244 msgid "Such as billing history, subscription status, or cancellation logs." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-document-fields.ts:281 msgid "A receipt from the shipping carrier or a tracking number, for example." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-document-fields.ts:29 msgid "Upload evidence" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-document-fields.ts:30 msgid "Submit any files you find relevant to this dispute." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-document-fields.ts:84 msgid "Any correspondence with the customer regarding this purchase." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-documents.tsx:22 msgid "Recommended documents" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-documents.tsx:23 msgid "" "While optional, we strongly recommend providing as many of these documents " "as possible. The following file types are supported: PDF, JPEG, and PNG." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/recommended-documents.tsx:25 msgid "Learn more about documents" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/refund-status.tsx:17 msgid "Refund status" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/refund-status.tsx:21 msgid "Refund has been issued" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/refund-status.tsx:25 msgid "Refund was not owed" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/shipping-details.tsx:12 msgid "Delivery details" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/shipping-details.tsx:13 msgid "" "Please ensure all prefilled information is correct and complete any missing " "details." msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/shipping-details.tsx:15 msgid "SHIPPING CARRIER" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/shipping-details.tsx:17 msgid "SHIPPING DATE" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/shipping-details.tsx:23 msgid "TRACKING NUMBER" msgstr "" #: dist/index.js:1 #: client/disputes/new-evidence/shipping-details.tsx:25 msgid "SHIPPING ADDRESS" msgstr "" #: dist/index.js:1 #: client/disputes/redirect-to-transaction-details/index.tsx:39 msgid "Error retrieving dispute" msgstr "" #: dist/index.js:1 #: client/disputes/redirect-to-transaction-details/index.tsx:40 msgid "Please check your network and try again." msgstr "" #: dist/index.js:1 #: client/disputes/redirect-to-transaction-details/index.tsx:45 msgid "One moment please" msgstr "" #: dist/index.js:1 #: client/disputes/redirect-to-transaction-details/index.tsx:46 msgid "Redirecting to payment details…" msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:101 msgid "The customer claims they did not receive the products or services purchased." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:104 msgid "" "Prove that the customer received a physical product or offline service, or " "made use of a digital product or online service. This must have occurred " "prior to the date the dispute was initiated." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:107 msgid "" "First, get in touch with your customer. Understanding why they filed the " "dispute will be important for helping make sure your customer gets the " "product and will give you critical information to prevent this from " "happening to others." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:110 msgid "The cardholder claims they did not receive the product." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:116 msgid "" "If you can not prove the customer received their product or service as " "described, you should accept the dispute. You cannot issue a refund while a " "payment is being disputed. The credit card networks place liability for " "accepting disputed payments with you, the business." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:119 msgid "" "The product or service was received but was defective, damaged, or not as " "described." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:122 msgid "" "Demonstrate that the product or service was delivered as described at the " "time of purchase." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:125 msgid "" "If the product or service is as described, provide specific information " "(invoice, contract, etc.) to refute the cardholder’s claims. Quality " "disputes are where the customer does not agree with the condition of " "merchandise or service received (e.g., a car repair situation or quality of " "a hotel room). There may be instances where you will need to obtain a " "neutral third-party opinion to help corroborate your claim against the " "cardholder. Provide as much specific information and documentation as " "possible to refute the cardholder’s claims. It is recommended that you " "address each point that the cardholder has made." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:126 msgid "" "If the customer has not yet returned the product or canceled the service, " "provide specific information to that effect. You should double-check your " "incoming shipping records to verify that you have not received a return " "before you respond. If you have processed a credit or reversal for this " "transaction, provide evidence of this which includes the amount and date " "processed." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:127 msgid "" "For products that have been repaired or replaced, provide evidence that the " "cardholder agreed to a repair or replacement, it has been received by the " "customer, and the repair or replacement has not since been disputed." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:128 msgid "" "If your customer made no attempt to return the product or cancel the " "service, or if you provided a replacement product or service, make sure to " "note that as well." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:129 msgid "" "If the customer withdraws their dispute you should still submit evidence " "using the forms on the next screen. Be sure to provide a letter or email " "from the cardholder stating that they are no longer in dispute." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:131 msgid "The cardholder claims the product was unacceptable." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:140 msgid "" "The customer claims that you continued to charge them after a subscription " "was canceled." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:143 msgid "" "Prove that the subscription was still active and that the customer was " "aware of, and did not follow, your cancellation procedure." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:146 msgid "" "First, get in touch with your customer. If you understand what they believe " "happened, there is a chance for you to explain the misunderstanding or to " "make it right. " msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:147 msgid "" "Even if your customer agrees to withdraw the dispute, you must still submit " "appropriate evidence using the forms on the next screen. Simply saying that " "your customer is going to withdraw the dispute is not sufficient evidence." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:148 msgid "" "Otherwise, use the forms on the next screen to submit evidence that the " "subscription was still active and that the customer was aware of, and did " "not follow, your cancellation procedure." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:150 msgid "The cardholder claims a subscription was canceled." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:155 msgid "" "If you believe the dispute is invalid, you can challenge it by submitting " "the appropriate evidence using the response forms on the next screen." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:156 msgid "" "If you can not prove the customer’s subscription was canceled, and or they " "did not follow your cancellation policy, you should accept the dispute. You " "cannot issue a refund while a payment is being disputed. The credit card " "networks place liability for accepting disputed payments with you, the " "business." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:159 msgid "" "The customer doesn’t recognize the payment appearing on their card " "statement." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:162 msgid "" "Get your customer to withdraw the dispute by helping them identify the " "payment. Otherwise, challenge the dispute with appropriate evidence that " "proves the purchase was legitimate. " msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:165 msgid "" "First, try to get in touch with your customer. Sometimes people forget " "about payments they make or don’t recognize the way they appear on their " "card statement. If this is the case, ask them to contact their card issuer " "and let them know they no longer dispute the transaction. Even if your " "customer agrees to withdraw the dispute, you must still submit appropriate " "evidence. Simply saying that your customer is going to withdraw the dispute " "is not sufficient evidence." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:166 msgid "" "It may be more efficient—and provide a better customer experience—to accept " "an accidental dispute and charge the customer again, if appropriate. Even " "when a dispute is withdrawn, it usually takes approximately 75 days to be " "finalized. Remember, it doesn’t matter to the card networks whether you win " "or lose a dispute; what matters is how many disputes a business receives, " "regardless of how many disputes are won." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:171 msgid "Your customer’s bank claims this payment violates Visa’s rules." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:173 msgid "This transaction is being reviewed under Visa’s network compliance rules." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:20 msgid "" "If your customer was not refunded appropriately, you will need to accept " "the dispute, or resolve the issue with your customer. The credit card " "networks place liability for accepting disputed payments with you, the " "business." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:23 msgid "" "The customer claims that the purchased product was returned or the " "transaction was otherwise canceled, but you have not yet provided a refund " "or credit." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:26 msgid "" "Demonstrate that you have refunded your customer through other means or " "that your customer is not entitled to a refund. You cannot issue a refund " "while a payment is being disputed. If you believe that your customer was " "entitled a refund that you did not provide, you can accept the dispute." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:29 msgid "" "You should first get in touch with your customer. If you understand what " "their complaint is, there is a chance for you to explain the " "misunderstanding or to make it right. If you’re able to resolve the issue " "with your customer, you can ask that they withdraw the dispute." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:30 msgid "" "If the cardholder agrees to withdraw the dispute, you should still submit " "evidence for the dispute using the forms on the next screen. In addition to " "the following evidence, your submission should include correspondence with " "the cardholder saying they would withdraw the dispute and a written " "statement from their card issuer confirming that the dispute has been " "withdrawn." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:32 msgid "The cardholder claims a credit was not processed." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:44 msgid "" "If there were duplicate payments, you should accept the dispute. You cannot " "issue a refund while a payment is being disputed. The credit card networks " "place liability for accepting disputed payments with you, the business." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:47 msgid "" "The customer claims they were charged multiple times for the same product " "or service." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:50 msgid "Demonstrate that each payment was for a separate product or service." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:53 msgid "Determine if your customer was incorrectly charged multiple times." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:54 msgid "" "If they were not, collect any and all information documenting that each " "payment was made separately, such as copies of receipts. If the receipts " "don’t include the items purchased, be sure to include an itemized list. " "Each receipt should clearly indicate that the payments are for separate " "purchases of items or services. If you’ve been able to get in touch with " "the customer you should be sure to address any concerns they had in your " "evidence." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:55 msgid "" "If there have been two or more separate payments, you should get in touch " "with your customer. If you understand what their complaint is, there is a " "chance for you to explain the misunderstanding or to make it right. If " "you’re able to resolve the issue with your customer, you can ask that they " "withdraw the dispute." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:58 msgid "The cardholder claims this is a duplicate transaction." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:64 msgid "" "If you believe the payment was actually made using a stolen credit card, " "you will need to accept the dispute. The credit card networks place " "liability for accepting unauthorized transactions with you, the business." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:67 msgid "" "This is the most common reason for a dispute, and happens when a cardholder " "claims that they didn’t authorize the payment. This can happen if the card " "was lost or stolen and used to make an unauthorized transaction. It can " "also happen if the cardholder doesn’t recognize the payment as it appears " "on the billing statement from their card issuer." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:70 msgid "" "Provide adequate payment and order details so that a legitimate customer " "recognizes it, or proves to the card issuer that their cardholder " "authorized the transaction." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:73 msgid "" "Try to get in touch with your customer. Sometimes people forget about " "payments they make or don’t recognize the way they appear on their card " "statement. If this is the case, ask them to contact their card issuer and " "let them know they no longer dispute the transaction." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:77 msgid "The cardholder claims this is an unauthorized transaction." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:85 msgid "" "This is an uncategorized dispute, so you should contact the customer for " "additional details to find out why the payment was disputed." msgstr "" #: dist/multi-currency-switcher-block.js:1 #: dist/wc-payments-settings-spotlight.js:1 #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/order.js:1 #: dist/settings.js:1 #: client/disputes/strings.ts:98 msgid "" "If you can not prove the customer received their product or service, you " "should accept the dispute. You cannot issue a refund while a payment is " "being disputed. The credit card networks place liability for accepting " "disputed payments with you, the business." msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:25 msgid "All documents" msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:42 msgid "Documents match {{select /}} filters" msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:43 msgid "Documents match <select /> filters" msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:48 msgid "Remove document date filter" msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:49 msgid "Select a document date filter match" msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:54 msgid "Select a document date" msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:77 msgid "Remove document type filter" msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:78 msgid "Select a document type filter match" msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:81 #: client/transactions/filters/config.ts:204 msgid "{{title}}Type{{/title}} {{rule /}} {{filter /}}" msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:82 #: client/transactions/filters/config.ts:205 msgid "<title>Type</title> <rule /> <filter />" msgstr "" #: dist/index.js:1 #: client/documents/filters/config.ts:83 msgid "Select a document type" msgstr "" #: dist/index.js:1 #: client/documents/list/index.tsx:108 msgid "Unknown document type" msgstr "" #: dist/index.js:1 #: client/documents/list/index.tsx:125 msgid "Download" msgstr "" #: dist/index.js:1 #: client/documents/list/index.tsx:26 #: client/transactions/list/index.tsx:91 msgid "Date and time" msgstr "" #: dist/index.js:1 #: client/documents/list/index.tsx:44 msgid "Description" msgstr "" #: dist/index.js:1 #: client/documents/list/index.tsx:58 msgid "Tax invoice for %s to %s" msgstr "" #: dist/index.js:1 #: client/documents/list/index.tsx:60 msgid "Tax invoice without proper period dates" msgstr "" #: dist/index.js:1 #: client/documents/strings.ts:8 msgid "Tax Invoice" msgstr "" #: dist/index.js:1 #: client/embedded-components/index.tsx:64 msgid "" "Unable to start onboarding. If this problem persists, please contact " "support." msgstr "" #: dist/settings.js:1 #: client/express-checkout/blocks/components/express-checkout-preview.js:102 msgid "" "Failed to preview the Apple Pay or Google Pay button. Ensure your store " "uses HTTPS on a publicly available domain and you're viewing this page in a " "Safari or Chrome browser. Your device must be configured to use Apple Pay " "or Google Pay." msgstr "" #: dist/blocks-checkout.js:1 #: client/express-checkout/blocks/index.js:19 msgid "An easy, secure way to pay that's accepted on millions of stores." msgstr "" #: dist/blocks-checkout.js:1 #: client/express-checkout/blocks/index.js:51 msgid "Simplify checkout with fewer steps to pay." msgstr "" #: dist/blocks-checkout.js:1 #: client/express-checkout/blocks/index.js:86 msgid "Pay with your Amazon account." msgstr "" #: dist/express-checkout.js:1 #: dist/blocks-checkout.js:1 #: client/express-checkout/event-handlers.js:201 msgid "There was a problem processing the order." msgstr "" #: dist/express-checkout.js:1 #: client/express-checkout/index.js:303 msgid "" "There was an error processing the product with this payment method. Please " "add the product to the cart, instead." msgstr "" #: dist/express-checkout.js:1 #: dist/blocks-checkout.js:1 #: dist/index.js:1 #: client/express-checkout/transformers/wc-to-stripe.js:142 #: client/payment-details/summary/index.tsx:215 #: client/transactions/list/index.tsx:145 msgid "Fees" msgstr "" #: dist/express-checkout.js:1 #: dist/blocks-checkout.js:1 #: dist/index.js:1 #: client/express-checkout/transformers/wc-to-stripe.js:158 #: client/payment-details/summary/missing-order-notice/index.tsx:17 #: client/transactions/strings.ts:12 msgid "Refund" msgstr "" #: dist/index.js:1 #: client/hooks/use-report-export.ts:110 #: client/transactions/blocked/index.tsx:113 msgid "There was a problem generating your export." msgstr "" #: dist/index.js:1 #: client/index.js:221 #: client/payment-details/readers/index.js:140 msgid "Card readers" msgstr "" #: dist/index.js:1 #: client/index.js:45 msgid "Connect" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/index.tsx:71 msgid "Are you satisfied with WooPayments?" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/index.tsx:78 msgid "\"Yes\" icon" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/index.tsx:86 msgid "\"No\" icon" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/negative-modal.tsx:20 #: client/merchant-feedback-prompt/positive-modal.tsx:18 msgid "Share your feedback" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/negative-modal.tsx:25 msgid "" "Thanks for sharing your feedback on WooPayments! Your feedback helps us to " "continue to improve and deliver the best tools for your business." msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/negative-modal.tsx:26 msgid "Would you mind sharing more about why you chose that option?" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/negative-modal.tsx:27 msgid "Share your feedback here…" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/negative-modal.tsx:30 #. {{a}}: placeholders are opening and closing anchor tags. msgid "" "Your feedback will be sent to the WooCommerce team. Your personal " "information is secure and will not be shared with third parties. For more " "details, please see our {{a/}}." msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/negative-modal.tsx:32 msgid "privacy policy" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/negative-modal.tsx:45 msgid "Contact our support team" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/negative-modal.tsx:56 msgid "Thank you for your feedback!" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/negative-modal.tsx:58 msgid "Send" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/positive-modal.tsx:22 msgid "" "Thanks for sharing your feedback on WooPayments! Would you mind leaving us " "a 5-star rating and a quick review on WordPress.org?" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/positive-modal.tsx:31 msgid "Help other businesses succeed" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/positive-modal.tsx:32 msgid "Your insights guide others in choosing the right payment solution." msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/positive-modal.tsx:39 msgid "Shape our roadmap" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/positive-modal.tsx:40 msgid "" "Your feedback inspires us to create new features and refine existing ones " "to better serve you." msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/positive-modal.tsx:47 msgid "Supporting the WooCommerce community" msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/positive-modal.tsx:48 msgid "" "Sharing your experience strengthens the tools that empower your fellow " "entrepreneurs." msgstr "" #: dist/index.js:1 #: client/merchant-feedback-prompt/positive-modal.tsx:57 msgid "Leave a review" msgstr "" #: dist/index.js:1 #: client/onboarding/steps/embedded-kyc.tsx:78 msgid "" "Payment activation through our financial partner requires HTTPS and cannot " "be completed." msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:16 msgid "Let’s get your store ready to accept payments" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:17 msgid "" "We’ll use these details to enable payments for your store. This information " "can’t be changed after your account is created." msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:20 msgid "Please share a few more details" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:21 msgid "This info will help us speed up the set up process." msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:25 msgid "" "This will take place in a secure environment through our partner. Once your " "business details are verified, you’ll be redirected back to your store " "dashboard." msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:26 msgid "Finish your verification process" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:29 msgid "One last step! Verify your identity with our partner" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:30 msgid "This info will verify your account" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:34 msgid "Where is your business located?" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:35 msgid "What type of legal entity is your business?" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:36 msgid "What category of legal entity identify your business?" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:37 msgid "What type of goods or services does your business sell? " msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:40 msgid "Please provide a response" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:41 msgid "Please provide a country" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:42 msgid "Please provide a business type" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:43 msgid "Please provide a type of goods or services" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:46 msgid "Select an option" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:47 msgid "Select a country" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:50 msgid "Less than $250k" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:51 msgid "$250k - $1M" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:52 msgid "$1M - $20M" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:53 msgid "$20M - $100M" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:54 msgid "More than $100M" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:57 msgid "My store is already live" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:58 msgid "Within 1 month" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:59 msgid "1 – 3 months" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:60 msgid "3 – 6 months" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:61 msgid "6+ months" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:64 msgid "" "By using %1$s, you agree to be bound by our {{tosLink}}Terms of " "Service{{/tosLink}} (including {{merchantTermsLink}}%2$s merchant " "terms{{/merchantTermsLink}}) and acknowledge that you have read our " "{{privacyPolicyLink}}Privacy Policy{{/privacyPolicyLink}}." msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:80 msgid "Business Location:" msgstr "" #: dist/index.js:1 #: client/onboarding/strings.tsx:81 msgid "Change" msgstr "" #: dist/index.js:1 #: client/onboarding/translations/descriptions.tsx:11 msgid "Select if you run a non-business entity" msgstr "" #: dist/index.js:1 #: client/onboarding/translations/descriptions.tsx:15 msgid "Select if you run your own business as an individual and are self-employed" msgstr "" #: dist/index.js:1 #: client/onboarding/translations/descriptions.tsx:16 msgid "" "Select if you filed documentation to register your business with a " "government agency" msgstr "" #: dist/index.js:1 #: client/onboarding/translations/descriptions.tsx:17 msgid "" "Select if you have been granted tax-exempt status by the Internal Revenue " "Service (IRS)" msgstr "" #: dist/index.js:1 #: client/onboarding/translations/descriptions.tsx:18 msgid "Select if your business is classed as a government entity" msgstr "" #: dist/order.js:1 #: client/order/index.js:28 msgid "Refunds and order editing are disabled during disputes." msgstr "" #: dist/order.js:1 #: client/order/index.js:33 msgid "Refunds and order editing are disabled during an active dispute." msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:102 msgid "Order cannot be refunded" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:106 msgid "Invalid Refund Amount" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:136 msgid "" "Are you trying to issue a refund for this order? If so, please click " "{{doNothingBold/}} and see our documentation on {{howtoIssueRefunds/}}. If " "you want to mark this order as Cancelled without issuing a refund, click " "{{cancelOrderBold/}}." msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:142 msgid "how to issue refunds" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:145 msgid "Do Nothing" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:145 msgid "Cancel order" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:17 msgid "completed" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:18 msgid "failed" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:19 msgid "on-hold" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:20 msgid "pending" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:21 msgid "processing" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:22 msgid "refunded" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:23 msgid "trash" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:52 msgid "" "This order has been {{authorizedNotCaptured/}} yet. Changing the status to " "{{newOrderStatus/}} will also {{cancelAuthorization/}}. Do you want to " "continue?" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:56 msgid "cancel the payment" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:60 msgid "Cancel payment" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:60 msgid "Cancel order and payment" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:77 msgid "" "This order has been {{authorizedNotCaptured/}} yet. Changing the status to " "{{newOrderStatus/}} will also {{captureAuthorization/}}. Do you want to " "continue?" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:80 msgid "authorized but payment has not been captured" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:81 msgid "capture the payment" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:85 msgid "Capture payment" msgstr "" #: dist/order.js:1 #: client/order/order-status-change-strategies/index.tsx:85 msgid "Complete order and capture payment" msgstr "" #: dist/order.js:1 #: client/order/refund-confirm-modal/index.js:115 msgid "Refund %s" msgstr "" #: dist/order.js:1 #: client/order/refund-confirm-modal/index.js:125 #. %s: WooPayments msgid "" "Issue a full refund back to your customer's payment method using %s. This " "action can not be undone. To issue a partial refund, click \"Cancel\", and " "use the \"Refund\" button in the order details below." msgstr "" #: dist/order.js:1 #: client/order/refund-confirm-modal/index.js:94 msgid "Refund order in full" msgstr "" #: dist/order.js:1 #: client/order/test-mode-notice/index.tsx:14 msgid "WooPayments was in test mode when this order was placed. {{learnMoreLink/}}" msgstr "" #: dist/order.js:1 #: dist/settings.js:1 #: client/order/test-mode-notice/index.tsx:16 #: client/settings/general-settings/test-mode-confirm-modal.tsx:24 msgid "Learn more about test mode" msgstr "" #: dist/index.js:1 #: client/overview/inbox-notifications/index.js:183 msgid "There was an error getting your inbox. Please try again." msgstr "" #: dist/index.js:1 #: client/overview/inbox-notifications/index.js:187 msgid "Reload" msgstr "" #: dist/index.js:1 #: client/overview/inbox-notifications/index.js:238 msgid "All messages dismissed" msgstr "" #: dist/index.js:1 #: client/overview/inbox-notifications/index.js:239 msgid "Message dismissed" msgstr "" #: dist/index.js:1 #: dist/tos.js:1 #: client/overview/inbox-notifications/index.js:243 #: client/overview/task-list/index.js:141 #: client/tos/disabled-notice.js:37 msgid "Undo" msgstr "" #: dist/index.js:1 #: client/overview/inbox-notifications/index.js:268 msgid "Message could not be dismissed" msgid_plural "Messages could not be dismissed" msgstr[0] "" msgstr[1] "" #: dist/index.js:1 #: client/overview/inbox-notifications/index.js:50 msgid "" "As things begin to happen in your store your inbox will start to fill up. " "You'll see things like achievements, new feature announcements, extension " "recommendations and more!" msgstr "" #: dist/index.js:1 #: client/overview/index.js:169 msgid "Success! You can start using WooPayments in test mode." msgstr "" #: dist/index.js:1 #: client/overview/index.js:208 msgid "Updates take a moment to appear. Please refresh the page in a minute." msgstr "" #: dist/index.js:1 #: client/overview/index.js:215 msgid "Refresh" msgstr "" #: dist/index.js:1 #: client/overview/index.js:253 msgid "" "There was a problem redirecting you to the loan offer. Please check that it " "is not expired and try again." msgstr "" #: dist/index.js:1 #: client/overview/index.js:261 msgid "" "There was a problem redirecting you to the requested link. Please check " "that it is valid and try again." msgstr "" #: dist/index.js:1 #: client/overview/index.js:269 msgid "" "There was a problem resetting your account. Please wait a few seconds and " "try again." msgstr "" #: dist/index.js:1 #: client/overview/index.js:297 msgid "" "Some account related notifications require HTTPS and cannot be displayed. " "View them on our financial partner’s website. {{seeDetailsLink}}See " "details{{/seeDetailsLink}}" msgstr "" #: dist/index.js:1 #: client/overview/index.js:54 msgid "" "There was a problem redirecting you to the account dashboard. Please try " "again." msgstr "" #: dist/index.js:1 #: client/overview/modal/connection-success/strings.tsx:8 msgid "You're ready to accept payments!" msgstr "" #: dist/index.js:1 #: client/overview/modal/connection-success/strings.tsx:9 msgid "" "Great news — your %s account has been activated. You can now start " "accepting payments on your store." msgstr "" #: dist/index.js:1 #: client/overview/modal/reset-account/strings.tsx:12 msgid "Reset your test account" msgstr "" #: dist/index.js:1 #: client/overview/modal/reset-account/strings.tsx:17 #. 1: WooPayments. msgid "" "When you reset your test account, all payment data — including your %1$s " "account details, test transactions, and payouts history — will be lost. " "Your order history will remain. This action cannot be undone, but you can " "create a new test account at any time." msgstr "" #: dist/index.js:1 #: client/overview/modal/reset-account/strings.tsx:20 #. 1: WooPayments. msgid "" "When you reset your account, all payment data — including your %1$s account " "details, test transactions, and payouts history — will be lost. Your order " "history will remain. This action cannot be undone, but you can create a new " "test account at any time." msgstr "" #: dist/index.js:1 #: client/overview/modal/reset-account/strings.tsx:21 msgid "Before you continue" msgstr "" #: dist/index.js:1 #: client/overview/modal/reset-account/strings.tsx:24 #. %s: WooPayments. msgid "Your %s account will be reset, and all data will be lost." msgstr "" #: dist/index.js:1 #: client/overview/modal/reset-account/strings.tsx:25 msgid "You will have to re-confirm your business and banking details." msgstr "" #: dist/index.js:1 #: client/overview/modal/reset-account/strings.tsx:26 msgid "Once confirmed, this cannot be undone." msgstr "" #: dist/index.js:1 #: client/overview/modal/reset-account/strings.tsx:27 msgid "Are you sure you want to continue?" msgstr "" #: dist/index.js:1 #: client/overview/modal/reset-account/strings.tsx:29 msgid "Yes, reset account" msgstr "" #: dist/index.js:1 #: client/overview/modal/update-business-details/strings.tsx:10 #: client/overview/task-list/tasks/update-business-details-task.tsx:114 #. %s: WooPayments msgid "Update %s business details" msgstr "" #: dist/index.js:1 #: client/overview/modal/update-business-details/strings.tsx:11 msgid "" "Payments and payouts are disabled for this account until missing " "information is updated. Please update the following information in the " "Stripe dashboard." msgstr "" #: dist/index.js:1 #: client/overview/modal/update-business-details/strings.tsx:12 msgid "" "Additional information is required to verify your business. Update by %s to " "avoid a disruption in payouts." msgstr "" #: dist/index.js:1 #: client/overview/modal/update-business-details/strings.tsx:13 msgid "Update business details" msgstr "" #: dist/index.js:1 #: dist/subscription-product-onboarding-modal.js:1 #: dist/subscriptions-empty-state.js:1 #: client/overview/modal/update-business-details/strings.tsx:7 #: client/overview/task-list/tasks/update-business-details-task.tsx:100 #: client/subscription-product-onboarding/modal.js:41 #: client/subscriptions-empty-state/index.js:74 msgid "Finish setup" msgstr "" #: dist/index.js:1 #: client/overview/task-list/index.js:100 msgid "Task deleted" msgstr "" #: dist/index.js:1 #: client/overview/task-list/index.js:137 msgid "Task postponed until tomorrow" msgstr "" #: dist/index.js:1 #: client/overview/task-list/index.js:161 msgid "Hide tasks" msgstr "" #: dist/index.js:1 #: client/overview/task-list/index.js:162 msgid "Show tasks" msgstr "" #: dist/index.js:1 #: client/overview/task-list/index.js:91 msgid "Task dismissed" msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:10 msgid "" "The combination of the city, state, and postal code in the provided address " "could not be validated." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:11 msgid "" "The street name and/or number for the provided address could not be " "validated." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:12 msgid "" "The existing terms of service signature has been invalidated because the " "account’s tax ID has changed. The account needs to accept the terms of " "service again. For more information, see <a>this documentation</a>." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:17 msgid "The representative must have an address in the same country as the company." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:18 msgid "" "The address on the document did not match the address on the account. " "Upload a document with a matching address or update the address on the " "account." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:19 msgid "" "The company address was missing on the document. Upload a document that " "includes the address." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:20 msgid "" "The uploaded file for the document was invalid or corrupt. Upload a new " "file of the document." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:21 msgid "The provided document was from an unsupported country." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:22 msgid "" "The date of birth (DOB) on the document did not match the DOB on the " "account. Upload a document with a matching DOB or update the DOB on the " "account." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:23 msgid "" "The same type of document was used twice. Two unique types of documents are " "required for verification. Upload two different documents." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:24 msgid "" "The document could not be used for verification because it has expired. If " "it’s an identity document, its expiration date must be after the date the " "document was submitted. If it’s an address document, the issue date must be " "within the last six months." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:25 msgid "" "The document could not be verified because it was detected as a copy (e.g., " "photo or scan). Upload the original document." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:26 msgid "" "The document could not be used for verification because it was in " "greyscale. Upload a color copy of the document." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:27 msgid "" "The document could not be verified for an unknown reason. Ensure that the " "document follows the <a>guidelines for document uploads</a>" msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:32 msgid "" "A test data helper was supplied to simulate verification failure. Refer to " "the documentation for <a>test file tokens</a>." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:39 msgid "" "The company ID number was missing on the document. Upload a document that " "includes the ID number." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:40 msgid "" "The document was cropped or missing important information. Upload a " "complete scan of the document." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:41 msgid "" "The uploaded file was not one of the valid document types. Ensure that the " "document follows the <a>guidelines for document uploads</a>." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:46 msgid "" "The issue or expiry date is missing on the document. Upload a document that " "includes the issue and expiry dates." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:48 msgid "" "The uploaded file was missing the back of the document. Upload a complete " "scan of the document." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:49 msgid "" "The uploaded file was missing the front of the document. Upload a complete " "scan of the document." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:50 msgid "" "The name on the document did not match the name on the account. Upload a " "document with a matching name or update the name on the account." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:51 msgid "" "The company name was missing on the document. Upload a document that " "includes the company name." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:52 msgid "" "The nationality on the document did not match the person’s stated " "nationality. Update the person’s stated nationality, or upload a document " "that matches it." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:53 msgid "" "The document could not be read. Ensure that the document follows the " "<a>guidelines for document uploads</a>." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:58 msgid "" "A valid signature is missing on the document. Upload a document that " "includes a valid signature." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:59 msgid "No document was uploaded. Upload the document again." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:60 msgid "The document was identified as altered or falsified." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:61 msgid "" "The uploaded file exceeded the 10 MB size limit. Resize the document and " "upload the new file." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:62 msgid "" "The provided document type was not accepted. Ensure that the document " "follows the <a>guidelines for document uploads</a>." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:67 msgid "" "The address on the account could not be verified. Correct any errors in the " "address field or upload a document that includes the address." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:68 msgid "" "The Importer Exporter Code (IEC) number could not be verified. Correct any " "errors in the company’s IEC number field. (India only)" msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:69 msgid "" "The document could not be verified. Upload a document that includes the " "company name, ID number, and address fields." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:70 msgid "" "The company ID number on the account could not be verified. Correct any " "errors in the ID number field or upload a document that includes the ID " "number." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:71 msgid "" "The person’s keyed-in identity information could not be verified. Correct " "any errors or upload a document that matches the identity fields (e.g., " "name and date of birth) entered." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:72 msgid "" "The keyed-in information on the account could not be verified. Correct any " "errors in the company name, ID number, or address fields. You can also " "upload a document that includes those fields." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:73 msgid "" "The company name on the account could not be verified. Correct any errors " "in the company name field or upload a document that includes the company " "name." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:74 msgid "" "We could not verify that the person resides at the provided address. The " "address must be a valid physical address where the individual resides and " "cannot be a P.O. Box." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:75 msgid "" "The tax ID on the account cannot be verified by the IRS. Either correct any " "possible errors in the company name or tax ID, or upload a document that " "contains those fields." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:76 msgid "" "The tax ID on the account was not recognized by the IRS. Refer to the " "support article for <a>newly-issued tax ID numbers</a>." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:81 msgid "" "Verification failed for an unknown reason. Correct any errors and resubmit " "the required fields." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:82 msgid "" "We have identified owners that haven’t been added on the account. Add any " "missing owners to the account." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:83 msgid "" "We have identified executives that haven’t been added on the account. Add " "any missing executives to the account." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:84 msgid "" "We have identified holding companies with significant percentage ownership. " "Upload a Memorandum of Association for each of the holding companies." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:85 msgid "Underage. Age must be at least 18." msgstr "" #: dist/index.js:1 #: client/overview/task-list/strings.tsx:90 msgid "10 minutes" msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/dispute-task.tsx:118 msgid "Respond to %d active disputes for a total of %s" msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/dispute-task.tsx:126 msgid "See disputes" msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/dispute-task.tsx:98 msgid "By %s – %s left to respond" msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/reconnect-task.tsx:24 #. %s: WooPayments msgid "Reconnect %s" msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/reconnect-task.tsx:27 #. %s: WooPayments msgid "" "%s is missing a connected WordPress.com account. Some functionality will be " "limited without a connected account." msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/reconnect-task.tsx:31 msgid "Reconnect" msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/update-business-details-task.tsx:103 msgid "Update" msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/update-business-details-task.tsx:111 #. %s: WooPayments msgid "Finish setting up %s" msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/update-business-details-task.tsx:27 #. %s - formatted requirements current deadline (date) msgid "Update by %s to avoid a disruption in payouts." msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/update-business-details-task.tsx:48 #. <a> - dashboard login URL msgid "Payments and payouts are disabled for this account until setup is completed." msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/update-business-details-task.tsx:53 #. <a> - dashboard login URL msgid "" "Payments and payouts are disabled for this account until missing business " "information is updated." msgstr "" #: dist/index.js:1 #: client/overview/task-list/tasks/update-business-details-task.tsx:97 msgid "More details" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:102 msgid "Please see this document for more information" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:104 msgid "Learn more about payment inquiries" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:107 msgid "Learn more about Visa compliance disputes" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:109 msgid "Learn more about responding to disputes" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:123 msgid "Unable to view order. Order not found." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:136 msgid "Submit evidence" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:159 msgid "" "By checking this box, you acknowledge that challenging this Visa compliance " "dispute incurs a $500 USD network fee, which will be refunded if you win " "the dispute." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:178 msgid "Continue with challenge" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:189 msgid "Before proceeding, please take note of the following:" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:34 msgid "Issue refund" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:36 msgid "Issue a refund?" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:40 msgid "" "Issuing a refund will close the inquiry, returning the amount in question " "back to the cardholder. No additional fees apply." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:44 msgid "" "You will be taken to the order, where you must complete the refund process " "manually." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:47 msgid "View order to issue refund" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:54 msgid "Accept the dispute?" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:60 #. %s: dispute fee, <em>: emphasis HTML element. msgid "" "Accepting the dispute marks it as <em>Lost</em>. The disputed amount and " "the %s dispute fee will not be returned to you." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:66 msgid "This action is final and cannot be undone." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:70 msgid "Accepting…" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-awaiting-response-details.tsx:71 msgid "Accept dispute" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-due-by-date.tsx:22 #. %d is the number of days left to respond to the dispute. msgid " (%d day left to respond)" msgid_plural " (%d days left to respond)" msgstr[0] "" msgstr[1] "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-due-by-date.tsx:24 msgid " (Last day today)" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-due-by-date.tsx:26 msgid " (Past due)" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-notice.tsx:18 msgid "The cardholder claims this is an unauthorized charge." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-notice.tsx:27 msgid "" "Klarna inquiries may mean that the customer is trying to return their " "item(s)." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-notice.tsx:33 msgid "" "<strong>%1$s</strong> If you believe this is incorrect, you have until " "<strong>%2$s to submit evidence to your customer's bank, %3$s.</strong> " "Alternatively, you can issue a refund." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-notice.tsx:34 msgid "" "<strong>%1$s</strong> If you believe this is incorrect, you have until " "<strong>%2$s to submit evidence to your customer's bank.</strong> " "Alternatively, you can issue a refund." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-notice.tsx:40 msgid "" "Your customer’s bank, %1$s, claims this payment violates Visa’s rules. " "<strong>You can challenge the dispute by %2$s, or accept it.</strong> If " "you accept the dispute, you will forfeit the funds and pay the dispute fee. " "Challenging adds an additional $500 USD dispute fee that is only returned " "to you if you win." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-notice.tsx:41 msgid "" "Your customer’s bank claims this payment violates Visa’s rules. <strong>You " "can challenge the dispute by %1$s, or accept it.</strong> If you accept the " "dispute, you will forfeit the funds and pay the dispute fee. Challenging " "adds an additional $500 USD dispute fee that is only returned to you if you " "win." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-notice.tsx:47 msgid "" "<strong>%1$s</strong> If you believe this is incorrect, you have until " "<strong>%2$s to challenge the dispute with your customer's bank, " "%3$s.</strong> If you accept the dispute, you will forfeit the funds and " "pay the dispute fee." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-notice.tsx:48 msgid "" "<strong>%1$s</strong> If you believe this is incorrect, you have until " "<strong>%2$s to challenge the dispute with your customer's bank.</strong> " "If you accept the dispute, you will forfeit the funds and pay the dispute " "fee." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:102 #. %1$s - formatted date msgid "<strong>You accepted this dispute on %1$s.</strong>" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:108 #. %1$s - formatted date msgid "" "<strong>Unfortunately, you've lost this dispute. Visa reached this decision " "on %1$s.</strong>" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:113 #. %1$s - bank name, %2$s - formatted date msgid "" "<strong>Unfortunately, you've lost this dispute. The customer's bank, %1$s, " "reached this decision on %2$s.</strong>" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:118 #. %s - formatted date msgid "" "<strong>Unfortunately, you've lost this dispute. The customer's bank " "reached this decision on %1$s.</strong>" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:130 #. %1$s – the formatted dispute fee amount msgid "" "The %1$s fee has been deducted from your account, and the disputed amount " "has been returned to your customer." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:144 msgid "View dispute details" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:156 #. %1$s - bank name, %2$s - formatted date msgid "" "You submitted evidence for this inquiry on %1$s. <strong>%2$s</strong> is " "reviewing the case, which can take 120 days or more. You will be alerted " "when they make their final decision." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:159 #. %s - formatted date msgid "" "You submitted evidence for this inquiry on %s. The <strong>cardholder’s " "bank</strong> is reviewing the case, which can take 120 days or more. You " "will be alerted when they make their final decision." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:163 msgid "Learn more." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:187 #. %s - formatted date msgid "This inquiry was closed on %s." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:189 msgid "Learn more about preventing disputes." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:201 msgid "View submitted evidence" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:24 #. %s - formatted date msgid "" "<strong>Visa is currently reviewing the evidence you submitted on " "%1$s.</strong> This process can sometimes take more than 60 days — we'll " "let you know once a decision has been made." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:30 #. %1$s - bank name, %2$s - formatted date msgid "" "<strong>The customer's bank, %1$s, is currently reviewing the evidence you " "submitted on %2$s.</strong> This process can sometimes take more than 60 " "days — we'll let you know once a decision has been made." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:33 #. %s - formatted date msgid "" "<strong>The customer's bank is currently reviewing the evidence you " "submitted on %1$s.</strong> This process can sometimes take more than 60 " "days — we'll let you know once a decision has been made." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:37 msgid "Learn more about the dispute process." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:61 #. %s - formatted date msgid "" "<strong>Good news — you've won this dispute! Visa reached this decision on " "%1$s.</strong> Your account has been credited with the disputed amount and " "fee." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:67 #. %1$s - bank name, %2$s - formatted date msgid "" "<strong>Good news — you've won this dispute! The customer's bank, %1$s, " "reached this decision on %2$s.</strong> Your account has been credited with " "the disputed amount and fee." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:70 #. %s - formatted date msgid "" "<strong>Good news — you've won this dispute! The customer's bank reached " "this decision on %1$s.</strong> Your account has been credited with the " "disputed amount and fee." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-resolution-footer.tsx:98 #. %1$s - formatted date msgid "This dispute was lost on %1$s due to non-response." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:128 msgid "Identify the issue and work towards a resolution where possible." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:134 msgid "Submit evidence or issue a refund" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:135 msgid "" "Disagree with the claim? You can challenge it by submitting evidence to the " "customer's bank. Otherwise, you can settle the inquiry by issuing a refund." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:167 msgid "Steps you can take" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:167 msgid "We recommend reviewing your options before responding by the deadline. " msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:175 msgid "Contact your customer" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:176 msgid "Reach out to the customer to check if they're returning the item(s)." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:177 msgid "Email customer" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:182 msgid "Issue a refund" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:183 msgid "" "Once you've received the item(s), refund the customer before the deadline " "to prevent this escalating to a dispute." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:188 msgid "Challenge the dispute" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:189 msgid "" "Didn't receive the returned item(s)? Once the inquiry has automatically " "escalated to a dispute after 21 days, you can submit evidence and challenge " "the dispute." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:37 msgid "We recommend reviewing your options before responding before the deadline. " msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:52 msgid "Ask for the dispute to be withdrawn" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:53 msgid "" "If you've managed to resolve the issue with your customer, help them with " "the withdrawal of their dispute." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:60 msgid "Challenge or accept the dispute" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:61 msgid "" "Disagree with the dispute? You can challenge it with the customer's bank. " "Otherwise, accept it to close the case — the order amount and dispute fee " "won't be refunded." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:80 msgid "Accepting the dispute" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:81 msgid "" "Accepting the dispute means you’ll forfeit the funds, pay the standard " "dispute fee, and avoid the $500 USD Visa network fee." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/dispute-steps.tsx:89 msgid "" "Challenging the dispute will incur a $500 USD Visa network fee, which is " "charged when you submit evidence. This fee will be refunded if you win the " "dispute." msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/evidence-list.tsx:32 #. Default filename for issuer evidence on a dispute msgid "Evidence.txt" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/evidence-list.tsx:56 msgid "Error downloading file" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/evidence-list.tsx:60 msgid "Loading" msgstr "" #: dist/index.js:1 #: client/payment-details/dispute-details/evidence-list.tsx:66 msgid "Issuer evidence" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-details/index.tsx:25 msgid "Payment details not loaded" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/amazon-pay/index.js:59 msgid "Amazon Transaction ID" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/amazon-pay/index.js:69 #: client/payment-details/payment-method/bancontact/index.js:92 #: client/payment-details/payment-method/base-payment-method-details/index.tsx:41 #: client/payment-details/payment-method/becs/index.js:83 #: client/payment-details/payment-method/card-present/index.js:128 #: client/payment-details/payment-method/card/index.js:153 #: client/payment-details/payment-method/eps/index.js:84 #: client/payment-details/payment-method/giropay/index.js:78 #: client/payment-details/payment-method/ideal/index.js:86 #: client/payment-details/payment-method/klarna/index.js:81 #: client/payment-details/payment-method/p24/index.js:95 #: client/payment-details/payment-method/sepa/index.js:79 #: client/payment-details/payment-method/sofort/index.js:92 msgid "ID" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/amazon-pay/index.js:78 #: client/payment-details/payment-method/bancontact/index.js:108 #: client/payment-details/payment-method/base-payment-method-details/index.tsx:43 #: client/payment-details/payment-method/becs/index.js:92 #: client/payment-details/payment-method/card-present/index.js:137 #: client/payment-details/payment-method/card/index.js:162 #: client/payment-details/payment-method/eps/index.js:100 #: client/payment-details/payment-method/giropay/index.js:87 #: client/payment-details/payment-method/ideal/index.js:123 #: client/payment-details/payment-method/klarna/index.js:104 #: client/payment-details/payment-method/p24/index.js:111 #: client/payment-details/payment-method/sepa/index.js:88 #: client/payment-details/payment-method/sofort/index.js:136 msgid "Owner" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/amazon-pay/index.js:85 #: client/payment-details/payment-method/bancontact/index.js:115 #: client/payment-details/payment-method/base-payment-method-details/index.tsx:44 #: client/payment-details/payment-method/becs/index.js:99 #: client/payment-details/payment-method/card-present/index.js:144 #: client/payment-details/payment-method/card/index.js:169 #: client/payment-details/payment-method/eps/index.js:107 #: client/payment-details/payment-method/giropay/index.js:94 #: client/payment-details/payment-method/ideal/index.js:130 #: client/payment-details/payment-method/klarna/index.js:111 #: client/payment-details/payment-method/p24/index.js:118 #: client/payment-details/payment-method/sepa/index.js:95 #: client/payment-details/payment-method/sofort/index.js:143 msgid "Owner email" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/payment-details/payment-method/amazon-pay/index.js:92 #: client/payment-details/payment-method/bancontact/index.js:122 #: client/payment-details/payment-method/base-payment-method-details/index.tsx:45 #: client/payment-details/payment-method/becs/index.js:106 #: client/payment-details/payment-method/card-present/index.js:151 #: client/payment-details/payment-method/card/index.js:176 #: client/payment-details/payment-method/eps/index.js:114 #: client/payment-details/payment-method/giropay/index.js:101 #: client/payment-details/payment-method/ideal/index.js:137 #: client/payment-details/payment-method/klarna/index.js:118 #: client/payment-details/payment-method/p24/index.js:125 #: client/payment-details/payment-method/sepa/index.js:102 #: client/payment-details/payment-method/sofort/index.js:150 #: client/vat/form/tasks/company-data-task.tsx:63 msgid "Address" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/bancontact/index.js:101 #: client/payment-details/payment-method/eps/index.js:91 #: client/payment-details/payment-method/ideal/index.js:116 #: client/payment-details/payment-method/p24/index.js:102 #: client/payment-details/payment-method/sofort/index.js:129 msgid "Verified name" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/bancontact/index.js:78 #: client/payment-details/payment-method/eps/index.js:77 #: client/payment-details/payment-method/giropay/index.js:64 #: client/payment-details/payment-method/ideal/index.js:93 #: client/payment-details/payment-method/p24/index.js:81 #: client/payment-details/payment-method/sofort/index.js:106 msgid "Bank name" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/bancontact/index.js:85 #: client/payment-details/payment-method/giropay/index.js:71 #: client/payment-details/payment-method/ideal/index.js:100 #: client/payment-details/payment-method/sofort/index.js:113 msgid "BIC" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/base-payment-method-details/index.tsx:30 #: client/payment-details/payment-method/card-present/index.js:75 #: client/payment-details/payment-method/card/index.js:89 #: client/payment-details/payment-method/eps/index.js:47 #: client/payment-details/payment-method/giropay/index.js:42 #: client/payment-details/payment-method/ideal/index.js:51 #: client/payment-details/payment-method/p24/index.js:49 #: client/payment-details/payment-method/sepa/index.js:49 #: client/payment-details/payment-method/sofort/index.js:59 msgid "id placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/base-payment-method-details/index.tsx:31 #: client/payment-details/payment-method/card-present/index.js:76 #: client/payment-details/payment-method/card/index.js:90 #: client/payment-details/payment-method/giropay/index.js:43 #: client/payment-details/payment-method/ideal/index.js:56 #: client/payment-details/payment-method/sepa/index.js:50 #: client/payment-details/payment-method/sofort/index.js:60 msgid "name placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/base-payment-method-details/index.tsx:32 #: client/payment-details/payment-method/card-present/index.js:77 #: client/payment-details/payment-method/card/index.js:91 #: client/payment-details/payment-method/eps/index.js:51 #: client/payment-details/payment-method/giropay/index.js:44 #: client/payment-details/payment-method/ideal/index.js:57 #: client/payment-details/payment-method/p24/index.js:54 #: client/payment-details/payment-method/sepa/index.js:51 #: client/payment-details/payment-method/sofort/index.js:61 msgid "email placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/base-payment-method-details/index.tsx:33 #: client/payment-details/payment-method/card-present/index.js:78 #: client/payment-details/payment-method/card/index.js:92 #: client/payment-details/payment-method/eps/index.js:52 #: client/payment-details/payment-method/giropay/index.js:45 #: client/payment-details/payment-method/ideal/index.js:58 #: client/payment-details/payment-method/p24/index.js:55 #: client/payment-details/payment-method/sepa/index.js:52 #: client/payment-details/payment-method/sofort/index.js:62 msgid "address placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/becs/index.js:69 msgid "BSB" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/becs/index.js:76 msgid "Account" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:107 #: client/payment-details/payment-method/card/index.js:128 msgid "Number" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:114 #: client/payment-details/payment-method/card/index.js:139 msgid "Expires" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:162 #: client/payment-details/payment-method/card/index.js:187 #: client/payment-details/payment-method/sepa/index.js:113 #: client/payment-details/payment-method/sofort/index.js:161 msgid "Origin" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:72 #: client/payment-details/payment-method/card/index.js:86 #: client/payment-details/payment-method/sepa/index.js:48 #: client/payment-details/payment-method/sofort/index.js:58 msgid "fingerprint placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:73 #: client/payment-details/payment-method/card/index.js:87 msgid "date placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:74 #: client/payment-details/payment-method/card/index.js:88 msgid "card type placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card-present/index.js:79 #: client/payment-details/payment-method/card/index.js:93 #: client/payment-details/payment-method/giropay/index.js:46 #: client/payment-details/payment-method/ideal/index.js:59 #: client/payment-details/payment-method/sepa/index.js:53 #: client/payment-details/payment-method/sofort/index.js:63 msgid "country placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card/check.js:14 msgid "Passed" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card/check.js:18 msgid "Unavailable" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card/check.js:20 msgid "Not checked" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card/index.js:194 msgid "CVC check" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card/index.js:201 msgid "Street check" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/card/index.js:210 #. Label for results of a postal code (ZIP code, in US) check performed by a credit card issuer. msgid "Postal code check" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/eps/index.js:48 #: client/payment-details/payment-method/giropay/index.js:40 #: client/payment-details/payment-method/ideal/index.js:52 #: client/payment-details/payment-method/p24/index.js:50 #: client/payment-details/payment-method/sofort/index.js:54 msgid "bank name placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/eps/index.js:49 #: client/payment-details/payment-method/p24/index.js:52 msgid "verified_name placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/eps/index.js:50 #: client/payment-details/payment-method/p24/index.js:53 msgid "customer_name placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/giropay/index.js:41 #: client/payment-details/payment-method/ideal/index.js:53 #: client/payment-details/payment-method/sofort/index.js:55 msgid "bic placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/ideal/index.js:107 #: client/payment-details/payment-method/sepa/index.js:72 #: client/payment-details/payment-method/sofort/index.js:120 msgid "IBAN" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/ideal/index.js:55 #: client/payment-details/payment-method/sofort/index.js:57 msgid "verified name placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/index.js:74 #: client/payment-details/summary/index.tsx:133 #: client/transactions/filters/config.ts:174 msgid "Payment method" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/klarna/index.js:28 msgid "pay_later" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/klarna/index.js:29 msgid "pay_now" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/klarna/index.js:30 msgid "pay_with_financing" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/klarna/index.js:31 msgid "pay_in_installments" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/klarna/index.js:88 msgid "Category" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/klarna/index.js:95 msgid "Preferred Locale" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/p24/index.js:51 msgid "reference placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/sofort/index.js:53 msgid "bank code placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/payment-method/sofort/index.js:99 msgid "Bank code" msgstr "" #: dist/index.js:1 #: client/payment-details/readers/index.js:40 msgid "Readers details not loaded" msgstr "" #: dist/index.js:1 #: client/payment-details/readers/index.js:65 msgid "Reader id" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:126 msgid "Subscription" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:137 msgid "Risk evaluation" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:182 msgid "a second" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:183 msgid "%d seconds" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:184 msgid "a minute" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:185 msgid "%d minutes" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:186 msgid "an hour" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:187 msgid "%d hours" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:188 msgid "a day" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:189 msgid "%d days" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:202 msgid "Amount placeholder" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:210 msgid "Deducted" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:214 msgid "Fee amount" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:217 msgid "Fee breakdown" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:220 msgid "Transaction fee" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:223 msgid "Dispute fee" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:226 msgid "Total fees" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:228 #: client/transactions/strings.ts:18 msgid "Loan repayment" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:231 msgid "Net amount" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:232 #: client/transactions/list/index.tsx:152 msgid "Net" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:247 msgid "Block transaction" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:255 msgid "Approve Transaction" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:259 msgid "Payment ID" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:262 msgid "Charge ID" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:265 msgid "Transaction actions" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:274 msgid "Refund in full" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:309 msgid "You must <a>capture</a> this charge within the next" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:326 msgid "Approving this transaction will capture the charge." msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:54 #: client/transactions/list/index.tsx:73 msgid "Tap to Pay on iPhone" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:57 #: client/transactions/list/index.tsx:76 msgid "Tap to Pay on Android" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:59 msgid "Tap to Pay" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/index.tsx:74 #: client/transactions/filters/config.ts:272 #: client/transactions/list/index.tsx:109 msgid "Sales channel" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/missing-order-notice/index.tsx:18 msgid "This transaction is not connected to order. " msgstr "" #: dist/index.js:1 #: client/payment-details/summary/missing-order-notice/index.tsx:20 msgid "It has been refunded and is not a subject for disputes." msgstr "" #: dist/index.js:1 #: client/payment-details/summary/missing-order-notice/index.tsx:21 msgid "Investigate this purchase and refund the transaction as needed." msgstr "" #: dist/index.js:1 #: client/payment-details/summary/refund-modal/index.tsx:43 msgid "Refund transaction" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/refund-modal/index.tsx:45 msgid "This will issue a full refund of {{strong}}%s{{/strong}} to the customer." msgstr "" #: dist/index.js:1 #: client/payment-details/summary/refund-modal/index.tsx:50 msgid "Select a reason (Optional)" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/refund-modal/index.tsx:52 msgid "Duplicate order" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/refund-modal/index.tsx:56 msgid "Fraudulent" msgstr "" #: dist/index.js:1 #: client/payment-details/summary/refund-modal/index.tsx:60 msgid "Requested by customer" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/index.js:30 msgid "Timeline" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/index.js:36 msgid "Error while loading timeline" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1001 msgid "Disputed: Needs response" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1014 msgid "Disputed: In review" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1018 msgid "Challenge evidence submitted." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1033 msgid "Disputed: Won" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1038 #. %s is a monetary amount msgid "Dispute reversal: %s" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1043 #. %s is a monetary amount msgid "Fee refund: %s" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1049 msgid "Dispute won! The bank ruled in your favor." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1060 msgid "Disputed: Lost" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1067 msgid "" "<strong>Dispute lost.</strong> Your customer's bank, <strong>%s</strong>, " "reviewed the evidence and decided in the customer's favor." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1074 msgid "" "<strong>Dispute lost.</strong> Your customer's bank reviewed the evidence " "and decided in the customer's favor." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1090 msgid "Dispute inquiry closed. The bank chose not to pursue this dispute." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1101 msgid "The disputed charge has been refunded." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:1116 msgid "Loan repayment: <a>Loan %s</a>" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:112 #. %s - formatted amount msgid "%s will be added to a future payout." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:117 #. %s - formatted amount msgid "%s will be deducted from a future payout." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:147 #. %1$s - formatted amount, %2$s - payout arrival date, <a> - link to the payout msgid "%1$s was subtracted from your <a>%2$s payout</a>." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:167 msgid "%s will be subtracted from a future payout." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:451 #. %s is a trcking reference number msgid "Acquirer Reference Number (ARN) %s" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:464 msgid "the card being expired or canceled." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:474 msgid "the card being lost or stolen." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:51 #. %s new status, for example Authorized, Refunded, etc msgid "Payment status changed to <strong>%s</strong>." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:557 msgid "Tap to pay transaction fee: %1$s%% + %2$s" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:562 msgid "Tap to pay transaction fee: %1$s%%" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:663 #. %s: the username that approved the payment, <a> - link to the user msgid "Payment was blocked by <a>%s</a>" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:665 #. %s: the username that approved the payment, <a> - link to the user msgid "Payment was approved by <a>%s</a>" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:711 msgid "Payment was screened by your fraud filters and blocked." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:715 msgid "Payment was screened by your fraud filters and placed in review." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:760 msgid "Started" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:767 msgid "Authorized" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:773 #. %s is a monetary amount msgid "A payment of %s was successfully authorized." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:787 msgid "Authorization voided" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:792 msgid "Authorization for %s was voided." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:807 msgid "Authorization expired" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:812 msgid "Authorization for %s expired." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:844 #. %s is a monetary amount msgid "A payment of %s was successfully charged." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:86 #. %1$s - formatted amount, %2$s - payout arrival date, <a> - link to the payout msgid "%1$s was added to your <a>%2$s payout</a>." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:878 msgid "A payment of %s was successfully refunded." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:901 msgid "%s refund was attempted but failed due to %s" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:91 #. %1$s - formatted amount, %2$s - payout arrival date, <a> - link to the payout msgid "%1$s was deducted from your <a>%2$s payout</a>." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:927 #. %1$s is the payment amount, %2$s is the failure reason message msgid "A payment of %1$s failed: %2$s." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:938 msgid "Payment disputed" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:945 #. %s is a monetary amount msgid "Payment disputed as %s." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:955 msgid "No funds have been withdrawn yet." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:960 msgid "" "The cardholder's bank is requesting more information to decide whether to " "return these funds to the cardholder." msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:985 #. %s is a monetary amount msgid "Disputed amount: %s" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/map-events.js:991 #. %s is a monetary amount msgid "Fee: %s" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:11 msgid "Place in review if the AVS verification fails" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:12 msgid "" "Place in review if the shipping address country differs from the billing " "address country" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:13 msgid "" "Place in review if the country resolved from customer IP is not listed in " "your selling countries" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:14 msgid "" "Place in review if the order originates from a country different from the " "shipping address country" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:15 msgid "Place in review if the items count is not in your defined range" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:16 msgid "Place in review if the purchase price is not in your defined range" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:19 msgid "Block if the AVS verification fails" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:20 msgid "Block if the shipping address differs from the billing address" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:21 msgid "" "Block if the country resolved from customer IP is not listed in your " "selling countries" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:22 msgid "" "Block if the order originates from a country different from the shipping " "address country" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:23 msgid "Block if the items count is not in your defined range" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:24 msgid "Block if the purchase price is not in your defined range" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:29 msgid "The card was declined by the bank" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:30 msgid "The card has expired" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:31 msgid "The security code is incorrect" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:32 msgid "The card number is incorrect" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:33 msgid "The postal code is incorrect" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:34 msgid "The security code is invalid" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:35 msgid "The expiration month is invalid" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:36 msgid "The expiration year is invalid" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:37 msgid "The card number is invalid" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:38 msgid "An error occurred while processing the card" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:39 msgid "The payment requires authentication" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:40 msgid "The card has insufficient funds to complete the purchase" msgstr "" #: dist/index.js:1 #: client/payment-details/timeline/mappings.ts:42 msgid "The payment was declined" msgstr "" #: client/payment-details/transaction-breakdown/fees-breakdown/index.tsx:77 msgid "Transaction fees breakdown" msgstr "" #: dist/index.js:1 #: client/payment-details/transaction-breakdown/index.tsx:56 msgid "Transaction breakdown" msgstr "" #: dist/index.js:1 #: client/payment-details/transaction-breakdown/index.tsx:61 msgid "Authorized payment" msgstr "" #: dist/index.js:1 #: client/payment-details/transaction-breakdown/index.tsx:69 msgid "Total transaction fee" msgstr "" #: dist/index.js:1 #: client/payment-details/transaction-breakdown/index.tsx:80 msgid "Net deposit" msgstr "" #: client/payment-details/transaction-breakdown/utils.ts:11 msgid "Total" msgstr "" #: client/payment-details/transaction-breakdown/utils.ts:13 msgid "Tax on fee" msgstr "" #: client/payment-details/transaction-breakdown/utils.ts:15 msgid "International payment fee" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/payment-details/transaction-breakdown/utils.ts:16 #: client/utils/account-fees.tsx:105 msgid "Currency conversion fee" msgstr "" #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/settings.js:1 #: client/payment-methods-icons.tsx:42 msgid "UnionPay" msgstr "" #: dist/multi-currency.js:1 #: dist/index.js:1 #: dist/settings.js:1 #: client/payment-methods-map.tsx:22 msgid "" "Let your customers pay with JCB, the only international payment brand based " "in Japan." msgstr "" #: dist/plugins-page.js:1 #: client/plugins-page/deactivation-survey/index.tsx:38 msgid "WooPayments Logo" msgstr "" #: dist/plugins-page.js:1 #: client/plugins-page/deactivation-survey/index.tsx:41 msgid "WooPayments Disable Survey" msgstr "" #: dist/settings.js:1 #: client/requirements-map.ts:6 msgid "Personal Identification Number" msgstr "" #: dist/settings.js:1 #: client/requirements-map.ts:7 msgid "Business Website" msgstr "" #: dist/settings.js:1 #: client/requirements-map.ts:8 msgid "Business Number" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/debug-mode.js:18 msgid "Debug mode" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/debug-mode.js:22 msgid "Log error messages (defaulted on for test accounts)" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/debug-mode.js:26 msgid "Log error messages" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/debug-mode.js:28 msgid "" "When enabled, payment error logs will be saved to WooCommerce > Status > " "Logs." msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/multi-currency-toggle.js:25 msgid "Enable Multi-Currency" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/multi-currency-toggle.js:27 msgid "" "Allow customers to shop and pay in multiple currencies. " "{{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx:43 msgid "" "There is currently %d customer subscription using Stripe Billing for " "payment processing. This subscription will be automatically migrated to use " "the on-site billing engine built into %s once Stripe Billing is disabled. " "{{learnMoreLink}}Learn more{{/learnMoreLink}}" msgid_plural "" "There are currently %d customer subscriptions using Stripe Billing for " "payment processing. These subscriptions will be automatically migrated to " "use the on-site billing engine built into %s once Stripe Billing is " "disabled. {{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr[0] "" msgstr[1] "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-notices/migrate-completed-notice.tsx:24 msgid "" "%d customer subscription was successfully migrated from Stripe off-site " "billing to on-site billing powered by %s and %s." msgid_plural "" "%d customer subscriptions were successfully migrated from Stripe off-site " "billing to on-site billing powered by %s and %s." msgstr[0] "" msgstr[1] "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx:65 msgid "Begin migration" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx:69 msgid "" "There is %d customer subscription using Stripe Billing for subscription " "renewals. We suggest migrating it to on-site billing powered by the %s " "plugin. {{learnMoreLink}}Learn more{{/learnMoreLink}}" msgid_plural "" "There are %d customer subscriptions using Stripe Billing for payment " "processing. We suggest migrating them to on-site billing powered by the %s " "plugin. {{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr[0] "" msgstr[1] "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-notices/migration-progress-notice.tsx:52 msgid "" "%d customer subscription is being migrated from Stripe off-site billing to " "billing powered by %s and %s." msgid_plural "" "%d customer subscriptions are being migrated from Stripe off-site billing " "to billing powered by %s and %s." msgstr[0] "" msgstr[1] "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-section.tsx:79 msgid "Subscriptions" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-section.tsx:82 msgid "Enable Stripe Billing" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-section.tsx:83 msgid "OK" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-section.tsx:84 msgid "" "Stripe Billing is not available with <b>manual capture enabled</b>. To use " "Stripe Billing, disable manual capture in your settings list." msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-toggle.tsx:19 msgid "Enable Stripe Billing for future subscriptions" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-toggle.tsx:22 msgid "" "Alternatively, you can enable this setting and future %s subscription " "purchases will also utilize Stripe Billing for payment processing. Note: " "This feature supports card payments only and may lack support for key " "subscription features. {{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/stripe-billing-toggle.tsx:26 msgid "" "By enabling this setting, future %s subscription purchases will utilize " "Stripe Billing for payment processing. Note: This feature supports card " "payments only and may lack support for key subscription features. " "{{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/wcpay-subscriptions-toggle.js:38 #. %s: WooPayments msgid "Enable Subscriptions with %s" msgstr "" #: dist/settings.js:1 #: client/settings/advanced-settings/wcpay-subscriptions-toggle.js:42 msgid "" "This feature is deprecated. Existing subscription renewals will continue to " "work, but creating or managing subscriptions is no longer available. " "Install {{learnMoreLink}}WooCommerce Subscriptions{{/learnMoreLink}} to " "continue managing subscriptions." msgstr "" #: dist/settings.js:1 #: client/settings/buy-now-pay-later-section/index.js:22 msgid "Buy now, pay later" msgstr "" #: dist/settings.js:1 #: client/settings/buy-now-pay-later-section/index.js:24 msgid "" "Boost sales by offering customers additional buying power and flexible " "payment options." msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:109 msgid "Frequency" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:128 msgid "Day" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:139 msgid "Payouts scheduled on a weekend will be sent on the next business day." msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:144 msgid "Payouts that fall on a holiday will initiate on the next business day." msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:149 msgid "Payouts will occur every business day." msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:169 msgid "" "Payout scheduling is currently unavailable for your store. " "{{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:230 msgid "Payout schedule" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:234 msgid "Payout bank account" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:239 msgid "Manage and update your bank account information to receive payouts." msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:256 msgid "Manage in Stripe" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:32 msgid "Monday" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:33 msgid "Tuesday" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:35 msgid "Wednesday" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:38 msgid "Thursday" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:39 msgid "Friday" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:88 msgid "Daily" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:92 msgid "Weekly" msgstr "" #: dist/settings.js:1 #: client/settings/deposits/index.js:96 msgid "Monthly" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/amazon-pay-settings.js:104 msgid "Enable Amazon Pay as an express payment button" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/amazon-pay-settings.js:108 msgid "" "Show Amazon Pay buttons on store pages for faster purchases. Customers with " "Amazon accounts can use their stored payment information." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/amazon-pay-settings.js:133 #: client/settings/express-checkout-settings/payment-request-settings.js:100 #: client/settings/express-checkout-settings/woopay-settings.js:152 msgid "Show on product page" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/amazon-pay-settings.js:152 #: client/settings/express-checkout-settings/payment-request-settings.js:119 #: client/settings/express-checkout-settings/woopay-settings.js:169 msgid "Show on cart page" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/amazon-pay-settings.js:171 #: client/settings/express-checkout-settings/payment-request-settings.js:138 #: client/settings/express-checkout-settings/woopay-settings.js:188 msgid "Show on checkout page" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/amazon-pay-settings.js:38 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:46 msgid "Small {{helpText}}(40 px){{/helpText}}" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/amazon-pay-settings.js:47 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:55 msgid "Medium {{helpText}}(48 px){{/helpText}}" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/amazon-pay-settings.js:56 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:64 msgid "Large {{helpText}}(55 px){{/helpText}}" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/amazon-pay-settings.js:72 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:150 msgid "Button size" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/express-checkout-settings-notices.tsx:22 #. %s: name of a button type (e.g., "WooPay") msgid "%s button" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/express-checkout-settings-notices.tsx:27 #. %1$s and %2$s: names of button types msgid "%1$s and %2$s buttons" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/express-checkout-settings-notices.tsx:34 #. %1$s: comma-separated list of button types, %2$s: last button type in the list msgid "%1$s, and %2$s buttons" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/express-checkout-settings-notices.tsx:62 #. %s: formatted list of button types that share these settings msgid "These settings will also apply to the %s on your store." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/express-checkout-settings-notices.tsx:63 msgid "" "Some appearance settings may be overridden in the express payment section " "of the Cart & Checkout blocks." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/file-upload.tsx:100 msgid "Replace" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/file-upload.tsx:34 msgid "The file you have attached is exceeding the maximum limit." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/file-upload.tsx:95 msgid "Upload custom logo" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:104 msgid "Recommended for dark or colored backgrounds with high contrast." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:110 msgid "Outline" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:112 msgid "" "Recommended for white or light-colored backgrounds with insufficient " "contrast." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:138 msgid "Call to action" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:139 msgid "" "Select a button label that fits best with the flow of purchase or payment " "experience on your store." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:156 msgid "Theme" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:163 #: includes/multi-currency/client/blocks/currency-switcher.js:255 msgid "Border radius" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:164 msgid "Controls the corner roundness of express payment buttons." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:173 msgid "Border radius, number input" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:200 msgid "Border radius, slider" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:224 msgid "See the preview of enabled express payment buttons." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:79 msgid "Buy with" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:83 msgid "Donate with" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:87 msgid "Book with" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/general-payment-request-button-settings.js:96 msgid "Recommended for white or light-colored backgrounds with high contrast." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/index.js:109 msgid "Configure the display of Apple Pay and Google Pay buttons on your store." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/index.js:136 msgid "Allow your customers to collect payments via Amazon Pay." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/index.js:151 msgid "Configure the display of Amazon Pay buttons on your store." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/index.js:170 msgid "Invalid express checkout method ID specified." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/index.js:35 msgid "Allow your customers to collect payments via WooPay." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/index.js:48 msgid "Checkout appearance" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/index.js:59 msgid "Configure the display of WooPay buttons on your store." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/index.js:94 msgid "Allow your customers to collect payments via Apple Pay and Google Pay." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/payment-request-button-preview.js:60 msgid "" "To preview the express checkout buttons, ensure your store uses HTTPS on a " "publicly available domain, and you're viewing this page in a Safari or " "Chrome browser. Your device must be configured to use Apple Pay or Google " "Pay." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/payment-request-button-preview.js:82 msgid "" "To preview the express checkout buttons, activate at least one express " "checkout." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/payment-request-settings.js:55 msgid "Enable Apple Pay / Google Pay as options in the payment methods list" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/payment-request-settings.js:59 msgid "" "Customers with Apple Pay or Google Pay enabled will be able to pay with " "their preferred wallet as options in the payment methods list." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/payment-request-settings.js:71 msgid "Enable Apple Pay / Google Pay as express payment buttons" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/payment-request-settings.js:75 msgid "" "Show express payment buttons on store pages for faster purchases. Customers " "with Apple Pay or Google Pay enabled will be able to pay with their " "preferred wallet." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:205 msgid "Checkout logo" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:208 msgid "" "Upload a custom logo. Upload a horizontal image with a white or transparent " "background for best results. Use a PNG or JPG image format. Recommended " "width: 512 pixels minimum." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:221 msgid "Checkout theme" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:237 msgid "Enable global theme support" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:247 msgid "" "When enabled, WooPay checkout will be themed with your store’s brand colors " "and fonts. {{docs}}Learn more {{/docs}}" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:270 msgid "Checkout policies" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:275 msgid "" "Override the default {{privacyLink}}privacy policy{{/privacyLink}} and " "{{termsLink}}terms of service{{/termsLink}}, or add custom text to WooPay " "checkout. {{learnMoreLink}}Learn more{{/learnMoreLink}}." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:305 msgid "Preview of checkout" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:77 msgid "Enable WooPay" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:84 msgid "When enabled, customers will be able to checkout using WooPay." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout-settings/woopay-settings.js:89 msgid "" "When enabled, customers will be able to checkout using WooPay. In order to " "use {{wooPayLink}}WooPay{{/wooPayLink}}, you must agree to our " "{{tosLink}}WooCommerce Terms of Service{{/tosLink}} and " "{{privacyLink}}Privacy Policy{{/privacyLink}}. {{trackingLink}}Click " "here{{/trackingLink}} to learn more about the data you will be sharing and " "opt-out options." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout/amazon-pay-item.tsx:32 msgid "" "By activating this feature, you accept {{stripeLink}}Stripe{{/stripeLink}} " "and {{amazonLink}}Amazon{{/amazonLink}}'s terms of use." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout/amazon-pay-item.tsx:45 #: client/settings/express-checkout/apple-google-pay-item.tsx:82 #: client/settings/express-checkout/woopay-item.tsx:66 msgid "Customize" msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout/apple-google-pay-item.tsx:42 msgid "" "By enabling this feature, you agree to {{stripeLink}}Stripe{{/stripeLink}} " "and{{appleLink}} Apple{{/appleLink}}'s terms of use." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout/apple-google-pay-item.tsx:69 msgid "" "By enabling this feature, you agree to {{stripeLink}}Stripe{{/stripeLink}}, " "and {{googleLink}}Google{{/googleLink}}'s terms of use." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout/link-item.tsx:59 msgid "" "By enabling this feature, you agree to the {{stripeLinkTerms}}Link by " "Stripe terms{{/stripeLinkTerms}}, and {{privacyPolicy}}Privacy " "Policy{{/privacyPolicy}}." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout/link-item.tsx:74 msgid "To enable Link by Stripe, you must first disable WooPay." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout/woopay-item.tsx:42 msgid "" "Boost conversion and customer loyalty by offering a single click, secure " "way to pay." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout/woopay-item.tsx:45 msgid "" "Boost conversion and customer loyalty by offering a single click, secure " "way to pay. In order to use {{wooPayLink}}WooPay{{/wooPayLink}}, you must " "agree to our {{tosLink}}WooCommerce Terms of Service{{/tosLink}} and " "{{privacyLink}}Privacy Policy{{/privacyLink}}. {{trackingLink}}Click " "here{{/trackingLink}} to learn more about the data you will be sharing and " "opt-out options." msgstr "" #: dist/settings.js:1 #: client/settings/express-checkout/woopay-item.tsx:67 msgid "To enable WooPay, you must first disable Link by Stripe." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/allow-countries-notice.tsx:16 msgid "Orders from the following countries will be blocked by the filter: " msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/allow-countries-notice.tsx:17 msgid "Orders from the following countries will be screened by the filter: " msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/allow-countries-notice.tsx:21 msgid "" "Orders from outside of the following countries will be blocked by the " "filter: " msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/allow-countries-notice.tsx:22 msgid "" "Orders from outside of the following countries will be screened by the " "filter: " msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/address-mismatch.tsx:12 msgid "Address Mismatch" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/address-mismatch.tsx:13 msgid "Enable Address Mismatch filter" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/address-mismatch.tsx:13 msgid "" "This filter screens for differences between the shipping information and " "the billing information (country). When enabled the payment will be blocked." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/address-mismatch.tsx:15 msgid "" "There are legitimate reasons for a billing/shipping mismatch with a " "customer purchase, but a mismatch could also indicate that someone is using " "a stolen identity to complete a purchase." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/avs-mismatch.tsx:13 msgid "AVS Mismatch" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/avs-mismatch.tsx:14 msgid "Enable AVS Mismatch filter" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/avs-mismatch.tsx:14 msgid "" "This filter compares the post code submitted by the customer against the " "post code on file with the card issuer. The payment will be blocked if the " "two post codes do not match." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/avs-mismatch.tsx:16 msgid "" "Buyers who can provide correct post code on file with the issuing bank are " "more likely to be the actual account holder." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx:17 msgid "CVC Verification" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx:20 msgid "" "For security, this filter is enabled and cannot be modified. Payments " "failing CVC verification will be blocked. {{learnMoreLink}}Learn " "more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx:32 msgid "This filter is disabled, and cannot be modified." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/cvc-verification.tsx:33 msgid "" "Because the card security code appears only on the card and not on receipts " "or statements, the card security code provides some assurance that the " "physical card is in the possession of the buyer." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/international-ip-address.tsx:20 msgid "International IP Address" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/international-ip-address.tsx:21 msgid "This filter is disabled because you're currently selling to all countries." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/international-ip-address.tsx:22 msgid "Enable International IP Address filter" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/international-ip-address.tsx:23 msgid "" "This filter screens for {{ipAddressLink}}IP addresses{{/ipAddressLink}} " "outside of your {{supportedCountriesLink}}supported " "countries{{/supportedCountriesLink}}. When enabled the payment will be " "blocked." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/international-ip-address.tsx:37 msgid "" "You should be especially wary when a customer has an international IP " "address but uses domestic billing and shipping information. Fraudsters " "often pretend to live in one location, but live and shop from another." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/ip-address-mismatch.tsx:14 msgid "IP Address Mismatch" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/ip-address-mismatch.tsx:15 msgid "Enable IP Address Mismatch filter" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/ip-address-mismatch.tsx:16 msgid "" "This filter screens for customer's {{ipAddressLink}}IP " "address{{/ipAddressLink}} to see if it is in a different country than " "indicated in their billing address. When enabled the payment will be " "blocked." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/ip-address-mismatch.tsx:24 msgid "" "Fraudulent transactions often use fake addresses to place orders. If the IP " "address seems to be in one country, but the billing address is in another, " "that could signal potential fraud." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:31 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:45 msgid "Limits" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:34 msgid "Minimum items per order" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:36 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:50 msgid "Leave blank for no limit" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:36 msgid "Maximum items per order" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:38 msgid "An item range must be set for this filter to take effect." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:40 msgid "Maximum item count must be greater than the minimum item count." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:42 msgid "Order Items Threshold" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:43 msgid "Enable Order Items Threshold filter" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:43 msgid "" "This filter compares the amount of items in an order to the minimum and " "maximum counts that you specify. When enabled the payment will be blocked." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:46 msgid "" "An unusually high item count, compared to the average for your business, " "can indicate potential fraudulent activity." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:52 msgid "An item range must be set for the \"Order Item Threshold\" filter." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/order-items-threshold.tsx:56 msgid "" "Maximum item count must be greater than the minimum item count on the " "\"Order Item Threshold\" rule." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:48 msgid "Minimum purchase price" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:50 msgid "Maximum purchase price" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:52 msgid "A price range must be set for this filter to take effect." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:56 msgid "Purchase Price Threshold" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:57 msgid "Enable Purchase Price Threshold filter" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:57 msgid "" "This filter compares the purchase price of an order to the minimum and " "maximum purchase amounts that you specify. When enabled the payment will be " "blocked." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:60 msgid "" "An unusually high purchase amount, compared to the average for your " "business, can indicate potential fraudulent activity." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:68 msgid "A price range must be set for the \"Purchase Price threshold\" filter." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/cards/purchase-price-threshold.tsx:72 msgid "Maximum purchase price must be greater than the minimum purchase price." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/index.tsx:175 msgid "" "There are unsaved changes on this page. Are you sure you want to leave and " "discard the unsaved changes?" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/index.tsx:193 msgid "Settings were not saved." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/settings/fraud-protection/advanced-settings/index.tsx:194 #: client/settings/fraud-protection/protection-levels/index.tsx:94 msgid "" "There was an error retrieving your fraud protection settings. Please " "refresh the page to try again." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/settings/fraud-protection/advanced-settings/index.tsx:215 #: client/settings/save-settings-section/index.js:117 #: includes/multi-currency/client/settings/multi-currency/store-settings/index.js:189 #: includes/multi-currency/client/settings/single-currency/index.js:416 msgid "Save changes" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/index.tsx:43 msgid "Filter configuration" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/index.tsx:44 msgid "" "Set up advanced fraud filters. Enable at least one filter to activate " "advanced protection." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/index.tsx:57 msgid "Advanced fraud protection" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/index.tsx:99 msgid "At least one risk filter needs to be enabled for advanced protection." msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/rule-description.tsx:12 msgid "How does this filter protect me?" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/rule-toggle.tsx:18 msgid "Authorize and hold for review" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/rule-toggle.tsx:22 msgid "Block Payment" msgstr "" #: dist/index.js:1 #: client/settings/fraud-protection/advanced-settings/rule-toggle.tsx:56 msgid "Filter action" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/index.tsx:17 msgid "Set your payment risk level" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/fp-help-text/index.tsx:10 #: client/settings/fraud-protection/protection-levels/protection-level-modal-notice/index.tsx:16 msgid "" "Offers the highest level of filtering for stores, but may catch some " "legitimate transactions." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/fp-help-text/index.tsx:12 msgid "" "Allows you to fine-tune the level of filtering according to your business " "needs." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/fp-help-text/index.tsx:14 msgid "Provides a standard level of filtering that's suitable for most businesses." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/fp-help-text/index.tsx:16 msgid "Provides the base level of platform protection." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/fp-modal/index.tsx:20 msgid "Basic filter level" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/fp-modal/index.tsx:25 msgid "Payments will be {{blocked}}blocked{{/blocked}} if:" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/fp-modal/index.tsx:31 msgid "The billing address does not match what is on file with the card issuer." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/fp-modal/index.tsx:32 msgid "The card's issuing bank cannot verify the CVV." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/index.tsx:101 msgid "Basic" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/index.tsx:102 msgid "Basic level help icon" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/index.tsx:110 msgid "Advanced" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/index.tsx:121 msgid "Edit" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/index.tsx:122 msgid "Configure" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/protection-level-modal-notice/index.tsx:14 msgid "Provides basic anti-fraud protection only." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/protection-levels/protection-level-modal-notice/index.tsx:15 msgid "Provides a standard level of filtering that's suitable for most business." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/tour/steps.tsx:13 msgid "Enhanced fraud protection 🔒" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/tour/steps.tsx:15 msgid "" "You can choose a level of protection for screening incoming transactions. " "Screened transactions will be automatically blocked by your customized " "fraud filters." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/tour/steps.tsx:18 msgid "See what's new" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/tour/steps.tsx:28 msgid "Choose your filter level 🚦" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/tour/steps.tsx:30 msgid "" "Choose how you'd like to screen incoming transactions using our Basic or " "Advanced options." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/tour/steps.tsx:40 msgid "Take more control 🎚️" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/tour/steps.tsx:42 msgid "" "Choose Advanced settings for full control over each filter. You can enable " "and configure filters to block risky transactions." msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/tour/steps.tsx:53 msgid "Review blocked transactions 📥️" msgstr "" #: dist/settings.js:1 #: client/settings/fraud-protection/tour/steps.tsx:56 msgid "" "Payments that have been blocked by a risk filter will appear under the " "blocked tab in {{strong}}Payments > Transactions{{/strong}}. We'll let you " "know why each payment was blocked so you can determine if you need to " "adjust your risk filters." msgstr "" #: dist/settings.js:1 #: client/settings/general-settings/enable-woopayments-checkbox.js:57 #. %s: WooPayments msgid "When enabled, payment methods powered by %s will appear on checkout." msgstr "" #: dist/settings.js:1 #: client/settings/general-settings/index.js:78 msgid "" "Use {{testCardHelpLink}}test card numbers{{/testCardHelpLink}} to simulate " "various transactions. {{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/settings.js:1 #: client/settings/general-settings/test-mode-confirm-modal.tsx:15 msgid "Enable" msgstr "" #: dist/settings.js:1 #: client/settings/general-settings/test-mode-confirm-modal.tsx:16 msgid "Are you sure you want to enable test mode?" msgstr "" #: dist/settings.js:1 #: client/settings/general-settings/test-mode-confirm-modal.tsx:17 msgid "" "Test mode lets you try out payments, refunds, disputes and other such " "processes as you're working on your store without handling live payment " "information. All incoming orders will be simulated, and test mode will have " "to be disabled before you can accept real orders." msgstr "" #: dist/settings.js:1 #: client/settings/notification-settings/index.tsx:15 msgid "Account notifications" msgstr "" #: dist/settings.js:1 #: client/settings/notification-settings/index.tsx:16 msgid "Receive important notifications about your WooPayments account." msgstr "" #: dist/settings.js:1 #: client/settings/notification-settings/notifications-email-input.tsx:37 msgid "Please enter a valid email address." msgstr "" #: dist/settings.js:1 #: client/settings/notification-settings/notifications-email-input.tsx:42 msgid "Notifications email" msgstr "" #: dist/settings.js:1 #: client/settings/notification-settings/notifications-email-input.tsx:43 msgid "" "Provide an email address where you would like to receive communications " "about your WooPayments account." msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/activation-modal.tsx:25 #. %s is the name of a payment method. msgid "One more step to enable %s" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/activation-modal.tsx:30 msgid "You need to provide more information to enable %s on your checkout:" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/activation-modal.tsx:36 msgid "You need to provide more information to enable %s on your checkout." msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/activation-modal.tsx:40 msgid "" "If you choose to continue, our payment partner Stripe will send an e-mail " "to {{merchantEmail /}} to collect the required information" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/delete-modal.tsx:15 msgid "Remove %1$s from checkout" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/delete-modal.tsx:18 #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/delete-button.js:71 msgid "Remove" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/delete-modal.tsx:22 msgid "" "Are you sure you want to remove {{strong}}%s{{/strong}}? Your customers " "will no longer be able to pay using %s." msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/delete-modal.tsx:29 msgid "You can add it again at any time in {{wooCommercePaymentsLink /}}." msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/payment-method.tsx:121 msgid "Base transaction fees: %s" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/payment-method.tsx:47 msgid "Discount details" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/payment-method.tsx:54 msgid "Promotion details" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/payment-method.tsx:65 msgid "Required" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:109 #. %s: a payment method name. msgid "" "%s is not available to your customers when the \"manual capture\" setting " "is enabled." msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:50 msgid "More information needed" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:53 #. {{learnMoreLink}}: placeholders are opening and closing anchor tags. msgid "" "We need more information from you to enable this method. " "{{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:58 msgid "Learn more about enabling payment methods" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:70 msgid "Approval pending" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:72 msgid "" "%s requires your store to be live and fully functional before it can be " "reviewed for use with their service. This approval process usually takes " "2-3 days." msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:73 msgid "" "This payment method is pending approval. Once approved, you will be able to " "use it." msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:79 msgid "Pending verification" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:81 msgid "" "%s won't be visible to your customers until you provide the required " "information. Follow the instructions sent by our partner Stripe to %s." msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:83 msgid "" "%s won't be visible to your customers until you provide the required " "information. Follow the instructions sent by our partner Stripe to your " "email." msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:90 msgid "Rejected" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:94 #. {{learnMoreLink}}: placeholders are opening and closing anchor tags. msgid "" "Your application to use %s has been rejected, please check your email for " "more information. Need help? {{contactSupportLink}}Contact " "support{{/contactSupportLink}}" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-list/use-payment-method-availability.tsx:98 msgid "Contact Support" msgstr "" #: dist/settings.js:1 #: client/settings/payment-methods-section/index.js:27 msgid "" "Add and edit payments available to customers at checkout. Based on their " "device type, location, and purchase history, your customers will only see " "the most relevant payment methods." msgstr "" #: dist/woopay.js:1 #: dist/settings.js:1 #: client/settings/phone-input/index.tsx:139 msgid "Mobile number" msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:103 msgid "Learn more about pending schedules" msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:117 msgid "" "Help avoid unauthorized transactions and disputes by setting your fraud " "protection level." msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:123 msgid "Learn more about fraud protection" msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:135 msgid "Advanced settings" msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:137 msgid "More options for specific payment needs." msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:143 msgid "View our documentation" msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:201 msgid "Tax details collection is not available for your account." msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:210 msgid "Tax details are already submitted." msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:227 msgid "Tax details updated" msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:42 msgid "Express checkouts" msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:44 msgid "" "Let your customers use their favorite express payment methods and digital " "wallets for faster, more secure checkouts across different parts of your " "store." msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:62 #. %s: WooPayments msgid "Enable or disable %s on your store." msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:76 msgid "Update your store's configuration to ensure smooth transactions." msgstr "" #: dist/settings.js:1 #: client/settings/settings-manager/index.js:95 msgid "Funds are available for payout %s business days after they’re received." msgstr "" #: dist/settings.js:1 #: client/settings/settings-warnings/incompatibility-notice.js:16 msgid "{{learnMoreLink}}Learn More{{/learnMoreLink}}" msgstr "" #: dist/settings.js:1 #: client/settings/settings-warnings/incompatibility-notice.js:36 msgid "One or more of your extensions are incompatible with WooPay." msgstr "" #: dist/settings.js:1 #: client/settings/support-email-input/index.js:21 msgid "Support email cannot be empty once it has been set before, please specify." msgstr "" #: dist/settings.js:1 #: client/settings/support-email-input/index.js:43 #: client/settings/support-phone-input/index.js:71 msgid "" "This may be visible on receipts, invoices, and automated emails from your " "store." msgstr "" #: dist/settings.js:1 #: client/settings/support-email-input/index.js:47 msgid "Support email" msgstr "" #: dist/settings.js:1 #: client/settings/support-phone-input/index.js:34 msgid "Support phone number cannot be empty." msgstr "" #: dist/settings.js:1 #: client/settings/support-phone-input/index.js:40 msgid "Please enter a valid phone number." msgstr "" #: dist/settings.js:1 #: client/settings/support-phone-input/index.js:47 msgid "" "Support phone number cannot be empty once it has been set before, please " "specify." msgstr "" #: dist/settings.js:1 #: client/settings/support-phone-input/index.js:59 msgid "Support phone number" msgstr "" #: dist/settings.js:1 #: client/settings/support-phone-input/index.js:78 msgid "(+1 0000000000 can be used for test accounts)" msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:105 msgid "Use only latin characters." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:129 msgid "Use only kanji characters." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:133 msgid "Customer bank statement (kanji)" msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:160 msgid "Use only kana characters." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:164 msgid "Customer bank statement (kana)" msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:192 msgid "Customer support" msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:194 msgid "Provide contact information where customers can reach you for support." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:66 msgid "Transaction preferences" msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:71 msgid "Enable payments via saved cards" msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:75 msgid "" "When enabled, users will be able to pay with a saved card during checkout. " "Card details are stored in our platform, not on your store." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:83 msgid "Customer statements" msgstr "" #: dist/settings.js:1 #: client/settings/transactions/index.js:85 msgid "Edit the way your store name appears on your customers' bank statements." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/manual-capture-control.tsx:39 msgid "" "Charge must be captured on the order details screen within 7 days of " "authorization, otherwise the authorization and order will be canceled." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/manual-capture-control.tsx:43 msgid "" " The setting is not applied to {{a}}In-Person Payments{{/a}} (please note " "that In-Person Payments should be captured within 2 days of authorization)." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/manual-capture-control.tsx:53 msgid "Manual capture is not available when Stripe Billing is active." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/manual-capture-control.tsx:56 msgid "Enable manual capture" msgstr "" #: dist/settings.js:1 #: client/settings/transactions/manual-capture-control.tsx:57 msgid "" "Payments must be captured within 7 days or the authorization will expire " "and money will be returned to the shopper." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/manual-capture-control.tsx:58 msgid "" "Additionally, only card payments support manual capture. Non-card payments " "will be hidden from checkout." msgstr "" #: dist/settings.js:1 #: client/settings/transactions/manual-capture-control.tsx:59 msgid "Do you want to continue?" msgstr "" #: dist/settings.js:1 #: client/settings/woopay-disable-feedback/index.js:23 msgid "WooPay Logo" msgstr "" #: dist/settings.js:1 #: client/settings/woopay-disable-feedback/index.js:35 msgid "WooPay Disable Feedback" msgstr "" #: dist/subscription-edit-page.js:1 #: client/subscription-edit-page/index.tsx:139 #: includes/multi-currency/client/blocks/currency-switcher.js:324 msgid "Loading…" msgstr "" #: dist/subscription-edit-page.js:1 #: client/subscription-edit-page/index.tsx:46 msgid "Failed to fetch user tokens" msgstr "" #: dist/subscription-edit-page.js:1 #: client/subscription-edit-page/index.tsx:51 msgid "Failed to fetch user tokens. Please reload the page and try again." msgstr "" #: dist/subscription-product-onboarding-modal.js:1 #: client/subscription-product-onboarding/modal.js:67 msgid "One more step to accept recurring payments" msgstr "" #: dist/subscription-product-onboarding-modal.js:1 #: client/subscription-product-onboarding/modal.js:75 #. %s: WooPayments msgid "" "Verify your business details with %s to accept recurring payments for this " "subscription product." msgstr "" #: dist/subscription-product-onboarding-modal.js:1 #: dist/subscriptions-empty-state.js:1 #: client/subscription-product-onboarding/modal.js:84 #: client/subscriptions-empty-state/index.js:37 msgid "By clicking \"Finish setup\", you agree to the <a>Terms of Service</a>" msgstr "" #: dist/subscription-product-onboarding-modal.js:1 #: client/subscription-product-onboarding/modal.js:98 msgid "We’ve saved your product as a draft." msgstr "" #: dist/subscription-product-onboarding-toast.js:1 #: client/subscription-product-onboarding/toast.js:30 #. %s: WooPayments msgid "" "Thank you for setting up %s! We’ve published your first subscription " "product." msgstr "" #: dist/subscriptions-empty-state.js:1 #: client/subscriptions-empty-state/index.js:24 #. %s: WooPayments msgid "" "Track recurring revenue and manage active subscriptions directly from your " "store’s dashboard — powered by %s." msgstr "" #: dist/subscriptions-empty-state.js:1 #: client/subscriptions-empty-state/index.js:89 msgid "Create subscription product" msgstr "" #: dist/tos.js:1 #: client/tos/disabled-notice.js:31 #. %s: WooPayments msgid "Disabled %s" msgstr "" #: dist/tos.js:1 #: client/tos/modal/index.tsx:28 #. %s: WooPayments msgid "%s: Terms of Service" msgstr "" #: dist/tos.js:1 #: client/tos/modal/index.tsx:32 #. %s: WooPayments msgid "" "To continue using %s, please review and agree to our {{link}}Terms of " "Service{{/link}}. By clicking “Accept” you agree to the Terms of Service." msgstr "" #: dist/tos.js:1 #: client/tos/modal/index.tsx:40 msgid "Something went wrong. Please try accepting the Terms of Service again!" msgstr "" #: dist/tos.js:1 #: client/tos/modal/index.tsx:44 msgid "Decline" msgstr "" #: dist/tos.js:1 #: client/tos/modal/index.tsx:45 msgid "Accept" msgstr "" #: dist/tos.js:1 #: client/tos/modal/index.tsx:54 #. %s: WooPayments msgid "" "By declining our {{link}}Terms of Service{{/link}}, you’ll no longer be " "able to capture credit card payments using %s. Your previous transaction " "and payout data will still be available." msgstr "" #: dist/tos.js:1 #: client/tos/modal/index.tsx:63 msgid "Something went wrong. Please try again!" msgstr "" #: dist/tos.js:1 #: client/tos/modal/index.tsx:68 msgid "Disable" msgstr "" #: dist/index.js:1 #: client/transactions/autocompleter.tsx:37 msgid "" "All transactions with customer names or billing emails that include {{query " "/}}" msgstr "" #: dist/index.js:1 #: client/transactions/blocked/columns.tsx:21 #: client/transactions/list/index.tsx:90 msgid "Date / Time" msgstr "" #: dist/index.js:1 #: client/transactions/blocked/index.tsx:56 msgid "Blocked transactions" msgstr "" #: dist/index.js:1 #: client/transactions/blocked/index.tsx:64 msgid "transactions(s)" msgstr "" #: dist/index.js:1 #: client/transactions/blocked/index.tsx:71 msgid "blocked" msgstr "" #: dist/index.js:1 #: client/transactions/blocked/index.tsx:93 msgid "Search by order number or customer name" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:112 msgid "Transactions match {{select /}} filters" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:113 msgid "Transactions match <select /> filters" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:118 msgid "Remove transaction date filter" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:119 msgid "Select a transaction date filter match" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:124 msgid "Select a transaction date" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:146 #: includes/multi-currency/client/analytics/index.js:123 msgid "Customer currency" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:147 msgid "Remove transaction customer currency filter" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:148 msgid "Select a transaction customer currency filter match" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:151 msgid "{{title}}Customer currency{{/title}} {{rule /}} {{filter /}}" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:152 msgid "<title>Customer currency</title> <rule /> <filter />" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:153 #: includes/multi-currency/client/analytics/index.js:36 msgid "Select a customer currency" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:175 msgid "Remove payment method filter" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:176 msgid "Select a payment method filter match" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:177 msgid "<title>Payment method</title> <rule /> <filter />" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:178 msgid "Select a payment method" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:200 msgid "Remove transaction type filter" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:201 msgid "Select a transaction type filter match" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:206 msgid "Select a transaction type" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:227 msgid "Loan" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:228 msgid "Remove loan filter" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:23 msgid "In Progress" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:232 msgid "{{title}}Loan{{/title}} {{rule /}} {{filter /}}" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:233 msgid "<title>Loan</title> <rule /> <filter />" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:234 msgid "Select a loan" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:24 msgid "Paid in Full" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:244 msgid "Device Type" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:245 msgid "Remove transaction device type filter" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:246 msgid "Select a transaction device type filter match" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:249 msgid "{{title}}Device type{{/title}} {{rule /}} {{filter /}}" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:250 msgid "<title>Device type</title> <rule /> <filter />" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:251 msgid "Select a transaction device type" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:273 msgid "Remove transaction sales channel filter" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:274 msgid "Select a transaction sales channel filter match" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:276 msgid "{{title}}Sales channel{{/title}} {{rule /}} {{filter /}}" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:277 msgid "<title>Sales channel</title> <rule /> <filter />" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:278 msgid "Select a transaction sales channel" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:299 msgid "Customer Country" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:300 msgid "Remove transaction customer country filter" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:301 msgid "Select a transaction customer country filter match" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:304 msgid "{{title}}Customer country{{/title}} {{rule /}} {{filter /}}" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:305 msgid "<title>Customer country</title> <rule /> <filter />" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:306 msgid "Select a transaction customer country" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:327 msgid "Risk Level" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:328 msgid "Remove transaction Risk Level filter" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:329 msgid "Select a transaction Risk Level filter match" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:332 msgid "{{title}}Risk Level{{/title}} {{rule /}} {{filter /}}" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:333 msgid "<title>Risk Level</title> <rule /> <filter />" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:334 msgid "Select a transaction Risk Level" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:42 msgid "Deposit currency" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:90 msgid "All transactions" msgstr "" #: dist/index.js:1 #: client/transactions/index.tsx:65 #. %1: number of uncaptured authorizations msgid "Uncaptured (%1$s)" msgstr "" #: dist/index.js:1 #: client/transactions/list/converted-amount.tsx:16 #. %s is a monetary amount msgid "Converted from %s" msgstr "" #: dist/index.js:1 #: client/transactions/list/deposit.tsx:29 msgid "Future payout" msgstr "" #: dist/index.js:1 #: client/transactions/list/deposit.tsx:31 msgid "" "This transaction will be included in an upcoming automated payout. The date " "of the payout will be displayed here once it is scheduled. " "{{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:115 msgid "Paid Currency" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:116 msgid "Customer Currency" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:122 msgid "Amount Paid" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:123 msgid "Amount in Customer Currency" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:131 msgid "Payout Currency" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:138 msgid "Amount in Payout Currency" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:160 #: client/transactions/uncaptured/index.tsx:47 msgid "Order number" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:166 msgid "Subscription number" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:197 #: client/transactions/uncaptured/index.tsx:52 msgid "Risk level" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:204 msgid "Payout ID" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:218 msgid "Payout status" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:389 msgid "Search by order number, subscription number, customer name, or billing email" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:390 msgid "Search by order number, customer name, or billing email" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:445 msgid "" "You are about to export %d transactions. If you'd like to reduce the size " "of your export, you can use one or more filters. Would you like to continue?" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:458 msgid "Search by customer name" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:471 msgid "transaction" msgid_plural "transactions" msgstr[0] "" msgstr[1] "" #: dist/index.js:1 #: client/transactions/list/index.tsx:487 msgid "fees" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:490 msgid "net" msgstr "" #: dist/index.js:1 #: client/transactions/list/index.tsx:84 msgid "Transaction ID" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:10 msgid "Payment failure refund" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:11 msgid "Payment refund" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:14 msgid "Dispute" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:15 msgid "Dispute reversal" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:16 msgid "Reader fee" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:17 msgid "Loan disbursement" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:19 msgid "Fee refund" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:23 msgid "Android" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:24 msgid "iPhone" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:28 #: client/utils/charge/index.ts:149 msgid "Online store" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:29 #: client/utils/charge/index.ts:147 msgid "In-Person" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:30 #: client/utils/charge/index.ts:145 msgid "In-Person (POS)" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:8 msgid "Charge" msgstr "" #: dist/index.js:1 #: client/transactions/strings.ts:9 msgid "Payment" msgstr "" #: dist/index.js:1 #: client/transactions/uncaptured/index.tsx:154 msgid "authorization(s)" msgstr "" #: dist/index.js:1 #: client/transactions/uncaptured/index.tsx:178 msgid "Uncaptured transactions" msgstr "" #: dist/index.js:1 #: client/transactions/uncaptured/index.tsx:27 msgid "Authorized on" msgstr "" #: dist/index.js:1 #: client/transactions/uncaptured/index.tsx:38 msgid "Capture by" msgstr "" #: dist/index.js:1 #: client/transactions/uncaptured/index.tsx:53 msgid "Risk level of transaction" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:102 msgid "International payment method fee" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:108 msgid "Total per transaction" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:117 #. %s: WooPayments msgid "" "{{linkToStripePage}}Learn more{{/linkToStripePage}} about %s Fees in your " "country" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:128 #. %s: WooPayments msgid "{{linkToStripePage /}} about %s Fees" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:140 #. %1: Percentage part of the fee. %2: Fixed part of the fee msgid "%1$f%% + %2$s per transaction" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:142 #. %f percentage discount to apply msgid "(%f%% discount)" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:166 #. %1 Base fee (that don't apply to this account at this moment), %2: Current fee (e.g: "2.9% + $.30 per transaction") msgid "<s>%1$s</s> %2$s" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:182 msgid "missing fees" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:185 #. %1: Percentage part of the fee. %2: Fixed part of the fee msgid "From %1$f%% + %2$s" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:197 msgid "Card transactions" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:199 msgid "In-person transactions" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:206 #. %s: Payment method title msgid "%s transactions" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:209 msgid "Unknown transactions" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:219 #. %1$s: discount percentage, %2$s: expiration date msgid "%1$s%% off fees through %2$s" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:223 #. %s: discount percentage msgid "%s%% off fees" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:235 #. %1$s: discount percentage, %2$s: total payment volume until this promotion expires, %3$s: End date of the promotion msgid "" "You are saving %1$s%% on processing fees for the first %2$s of total " "payment volume or through %3$s." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:240 #. %1$s: discount percentage, %2$s: total payment volume until this promotion expires msgid "" "You are saving %1$s%% on processing fees for the first %2$s of total " "payment volume." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:245 #. %1$s: discount percentage, %2$s: End date of the promotion msgid "You are saving %1$s%% on processing fees through %2$s." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/utils/account-fees.tsx:249 #. %s: discount percentage msgid "You are saving %s%% on processing fees." msgstr "" #: dist/settings.js:1 #: client/utils/format-list-of-items.ts:21 #. the first %s is a placeholder for the first item in a list of two. The last %s corresponds to the second item in the list. msgid "%s and %s" msgstr "" #: dist/settings.js:1 #: client/utils/format-list-of-items.ts:28 #. the first %s is a placeholder for a comma-separated list of one or more items. The last %s corresponds to the last item in the list. msgid "%s, and %s" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form-modal/index.tsx:13 msgid "Set your tax details" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/company-data-task.tsx:60 msgid "Confirm your business details" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/company-data-task.tsx:62 msgid "Business name" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/company-data-task.tsx:64 msgid "Confirm" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:110 #. %s: tax ID name, e.g. VAT Number, Corporate Number msgid "The provided %s failed validation." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:112 msgid "Your account's country is not supported for tax ID validation." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:170 msgid "Set your %1$s" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:173 msgid "" "The information you provide here will be used for all of your account's tax " "documents." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:175 msgid "I have a valid %1$s" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:50 msgid "ABN" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:52 msgid "Corporate Number" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:54 msgid "IRD Number" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:56 msgid "UEN or GST Registration Number" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:58 msgid "VAT Number" msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:64 msgid "" "By inputting your ABN number you confirm that you are going to account for " "the GST." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:70 msgid "" "By inputting your VAT number you confirm you are a Norway VAT registered " "business and that you are going to account for the VAT." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:72 msgid "" "By inputting your IRD number you confirm that you are going to account for " "the GST." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:74 msgid "" "By providing your UEN or GST number you confirm you are a Singapore GST " "registered business and you are going to account for the GST." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:78 msgid "" "If your sales exceed the VAT threshold for your country, you're required to " "register for a VAT Number." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:85 msgid "11-digit number, for example 12 345 678 901." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:87 msgid "13-digit number, for example 1234567890123." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:89 msgid "8-digit or 9-digit number, for example 99-999-999 or 999-999-999." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:91 msgid "" "Enter your UEN (e.g., 200312345A) or GST Registration Number (e.g., " "M91234567X)." msgstr "" #: dist/index.js:1 #: dist/settings.js:1 #: client/vat/form/tasks/vat-number-task.tsx:93 msgid "8 to 12 digits with your country code prefix, for example DE 123456789." msgstr "" #: includes/multi-currency/client/analytics/index.js:24 msgid "Remove customer currency filter" msgstr "" #: includes/multi-currency/client/analytics/index.js:28 msgid "Select a customer currency filter match" msgstr "" #: includes/multi-currency/client/analytics/index.js:32 msgid "{{title}}Customer Currency{{/title}} {{rule /}} {{filter /}}" msgstr "" #: dist/index.js:1 #: client/transactions/filters/config.ts:340 #. Sentence fragment, logical, "Is" refers to searching for transactions matching a chosen transaction risk level. msgctxt "Risk Level" msgid "Is" msgstr "" #: includes/multi-currency/client/analytics/index.js:51 #. Sentence fragment, logical, "Is Not" refers to searching for orders not matching a chosen currency. msgid "Is Not" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:210 msgid "Multi-Currency settings" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:220 msgid "Adjust and edit your Multi-Currency settings" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:226 msgid "Layout" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:228 msgid "Display flags" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:268 msgid "Typography" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:271 msgid "Size" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:280 msgid "Line height" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:294 msgid "Color settings" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:299 msgid "Text" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:30 msgid "Currency Switcher Block" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:305 msgid "Background" msgstr "" #: includes/multi-currency/client/blocks/currency-switcher.js:311 msgid "Border" msgstr "" #: includes/multi-currency/client/data/actions.js:108 msgid "Currency settings updated." msgstr "" #: includes/multi-currency/client/data/actions.js:112 msgid "Error updating currency settings." msgstr "" #: includes/multi-currency/client/data/actions.js:141 msgid "Store settings saved." msgstr "" #: includes/multi-currency/client/data/actions.js:145 msgid "Error saving store settings." msgstr "" #: includes/multi-currency/client/data/actions.js:83 msgid "Enabled currencies updated." msgstr "" #: includes/multi-currency/client/data/actions.js:92 msgid "Error updating enabled currencies." msgstr "" #: includes/multi-currency/client/data/resolvers.js:31 msgid "Error retrieving currencies." msgstr "" #: includes/multi-currency/client/data/resolvers.js:49 msgid "Error retrieving single currency settings." msgstr "" #: includes/multi-currency/client/data/resolvers.js:65 msgid "Error retrieving store settings." msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/delete-button.js:118 msgid "You can add %s (%s) again at any time in Multi-Currency settings." msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/delete-button.js:133 msgid "Remove %1$s as an enabled currency" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/delete-button.js:55 msgid "Remove %1$s" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/delete-button.js:86 msgid "" "Are you sure you want to remove {{strong}}%s (%s){{/strong}}? Your " "customers will no longer be able to pay in this currency and use payment " "methods listed below." msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/index.js:31 msgid "" "Accept payments in multiple currencies. Prices are converted based on " "exchange rates and rounding rules. <learnMoreLink>Learn more</learnMoreLink>" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/index.js:75 msgid "Name" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/index.js:76 #: includes/multi-currency/client/settings/single-currency/index.js:198 msgid "Exchange rate" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/list-item.js:72 msgid "Edit %1$s" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/list-item.js:82 msgid "manage" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/modal.js:136 msgid "Add enabled currencies" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/modal.js:156 msgid "Update selected" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/modal.js:169 #: includes/multi-currency/client/setup/tasks/add-currencies-task/index.js:251 msgid "Search currencies" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/modal.js:179 #: includes/multi-currency/client/setup/tasks/add-currencies-task/index.js:262 msgid "Search results (%1$d currencies)" msgstr "" #: includes/multi-currency/client/settings/multi-currency/enabled-currencies-list/modal.js:214 msgid "Add/remove currencies" msgstr "" #: includes/multi-currency/client/settings/multi-currency/store-settings/index.js:116 #: includes/multi-currency/client/setup/tasks/store-settings-task/index.js:133 msgid "" "Automatically switch customers to their local currency if it has been " "enabled" msgstr "" #: includes/multi-currency/client/settings/multi-currency/store-settings/index.js:121 msgid "" "Customers will be notified via store alert banner. " "<previewLink>Preview</previewLink>" msgstr "" #: includes/multi-currency/client/settings/multi-currency/store-settings/index.js:147 #: includes/multi-currency/client/setup/tasks/store-settings-task/index.js:156 msgid "Add a currency switcher to the Storefront theme on breadcrumb section." msgstr "" #: includes/multi-currency/client/settings/multi-currency/store-settings/index.js:154 #. %s: url to the widgets page msgid "" "A currency switcher is also available in your widgets. " "<linkToWidgets>Configure now</linkToWidgets>" msgstr "" #: includes/multi-currency/client/settings/multi-currency/store-settings/index.js:27 msgid "Store settings" msgstr "" #: includes/multi-currency/client/settings/multi-currency/store-settings/index.js:30 msgid "" "Store settings allow your customers to choose which currency they would " "like to use when shopping at your store. <learnMoreLink>Learn " "more</learnMoreLink>" msgstr "" #: includes/multi-currency/client/settings/single-currency/constants.js:32 msgid "None" msgstr "" #: includes/multi-currency/client/settings/single-currency/currency-preview.js:38 msgid "Please enter a valid number" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:143 msgid "" "Enter a price in your default currency (%s) to see it converted to %s using " "the exchange rate and formatting rules above." msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:225 msgid "Current rate: 1 %s = %s %s (Last updated: %s)" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:234 msgid "Error - Unable to fetch automatic rate for this currency" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:238 msgid "Fetch rates automatically" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:245 msgid "Enter your fixed rate of exchange" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:249 msgid "Manual" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:263 msgid "Manual Rate" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:267 msgid "Enter the manual rate you would like to use. Must be a positive number." msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:293 msgid "Formatting rules" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:303 msgid "Price rounding" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:309 msgid "" "Make your %s prices consistent by rounding them up after they're converted. " "{{learnMoreLink}}Learn more{{/learnMoreLink}}" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:347 msgid "Charm pricing" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:353 msgid "" "Reduce the converted price for a specific amount. {{learnMoreLink}}Learn " "more{{/learnMoreLink}}" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:46 msgid "Currency settings" msgstr "" #: includes/multi-currency/client/settings/single-currency/index.js:48 msgid "" "Choose between automatic or manual exchange rates and modify formatting " "rules to refine the display of your prices." msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/constants.js:10 msgid "three" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/constants.js:11 msgid "four" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/constants.js:12 msgid "five" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/constants.js:13 msgid "six" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/constants.js:14 msgid "seven" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/constants.js:15 msgid "eight" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/constants.js:16 msgid "nine" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/constants.js:7 msgid "any" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/constants.js:8 msgid "one" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/constants.js:9 msgid "two" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/index.js:189 msgid "Add currencies" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/index.js:191 msgid "%s currency added" msgid_plural "%s currencies added" msgstr[0] "" msgstr[1] "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/index.js:207 msgid "" "Add currencies so international customers can shop and pay in their local " "currency. Your store's default currency is {{storeCurrencyText /}}." msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/index.js:226 msgid "You've already added {{enabledCurrenciesText /}} to your store." msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/index.js:281 msgid "Recommended currencies" msgstr "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/index.js:78 msgid "Add %s currency" msgid_plural "Add %s currencies" msgstr[0] "" msgstr[1] "" #: includes/multi-currency/client/setup/tasks/add-currencies-task/utils.js:59 #. %1$s: comma-separated list of currencies, %2$s: last currency msgid "%1$s and %2$s" msgid_plural "%1$s, and %2$s" msgstr[0] "" msgstr[1] "" #: includes/multi-currency/client/setup/tasks/setup-complete-task/index.js:41 msgid "Setup complete" msgstr "" #: includes/multi-currency/client/setup/tasks/setup-complete-task/index.js:46 msgid "You're ready to begin accepting payments using foreign currencies!" msgstr "" #: includes/multi-currency/client/setup/tasks/setup-complete-task/index.js:53 msgid "" "Your product prices are automatically converted from your default currency " "(%s) based on the currency exchange rate and formatting rules for each " "currency." msgstr "" #: includes/multi-currency/client/setup/tasks/setup-complete-task/index.js:62 msgid "" "To enter your own exchange rates or update the formatting rules for each " "currency, visit the Multi-Currency settings." msgstr "" #: includes/multi-currency/client/setup/tasks/setup-complete-task/index.js:75 msgid "Back to home" msgstr "" #: includes/multi-currency/client/setup/tasks/setup-complete-task/index.js:82 msgid "View Multi-Currency settings" msgstr "" #: includes/multi-currency/client/setup/tasks/store-settings-task/index.js:101 msgid "{{wrapper}}Review store settings{{/wrapper}}" msgstr "" #: includes/multi-currency/client/setup/tasks/store-settings-task/index.js:117 msgid "" "These settings can be changed any time by visiting the Multi-Currency " "settings" msgstr "" #: includes/multi-currency/client/setup/tasks/store-settings-task/index.js:137 msgid "Customers will be notified via store alert banner." msgstr "" #: includes/multi-currency/client/setup/tasks/store-settings-task/index.js:160 msgid "A currency switcher is also available in your widgets." msgstr "" #: includes/multi-currency/client/utils/currency/index.js:10 msgid "Canadian dollar" msgstr "" #: includes/multi-currency/client/utils/currency/index.js:11 msgid "Swiss franc" msgstr "" #: includes/multi-currency/client/utils/currency/index.js:12 msgid "Danish krone" msgstr "" #: includes/multi-currency/client/utils/currency/index.js:13 msgid "Euro" msgstr "" #: includes/multi-currency/client/utils/currency/index.js:14 msgid "Pound sterling" msgstr "" #: includes/multi-currency/client/utils/currency/index.js:15 msgid "Norwegian krone" msgstr "" #: includes/multi-currency/client/utils/currency/index.js:16 msgid "New Zealand dollar" msgstr "" #: includes/multi-currency/client/utils/currency/index.js:17 msgid "Swedish krona" msgstr "" #: includes/multi-currency/client/utils/currency/index.js:18 msgid "United States (US) dollar" msgstr "" #: includes/multi-currency/client/utils/currency/index.js:9 msgid "Australian dollar" msgstr "" #: includes/multi-currency/client/utils/missing-currencies-message/index.ts:9 msgid "" "%1$s requires the %2$s currency. In order to enable the payment method, you " "must add this currency to your store." msgid_plural "" "%1$s requires the %2$s currencies. In order to enable the payment method, " "you must add these currencies to your store." msgstr[0] "" msgstr[1] "" #: dist/index.js:1 #: client/transactions/filters/config.ts:345 #. Sentence fragment, logical, "Is not" refers to searching for transactions that don\'t match a chosen transaction risk level. msgctxt "Risk Level" msgid "Is not" msgstr "" languages/README.md 0000777 00000000425 15251156605 0010007 0 ustar 00 # Languages ## Generating POT The generated POT template file is not included in this repository. It gets generated when building the project: ``` npm run build:client ``` After the build completes, you'll find a `woocommerce-payments.pot` strings file in this directory. includes/class-wc-payments-payment-method-messaging-element.php 0000777 00000014363 15251156605 0021014 0 ustar 00 <?php /** * Class WC_Payments_Payment_Method_Messaging_Element * * @package WooCommerce\Payments */ use WCPay\Constants\Payment_Method; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Payments_Payment_Method_Messaging_Element class. */ class WC_Payments_Payment_Method_Messaging_Element { /** * WC_Payments_Account instance to get information about the account. * * @var WC_Payments_Account */ private $account; /** * WC_Payments_Gateway instance to get information about the enabled payment methods. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * WC_Payments_Payment_Method_Messaging_Element constructor * * @param WC_Payments_Account $account Account instance. * @param WC_Payment_Gateway_WCPay $gateway Gateway instance. * @return void */ public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway ) { $this->account = $account; $this->gateway = $gateway; } /** * Initializes the payment method messaging element. * * @return string|void The HTML markup for the payment method message container. */ public function init() { $is_cart_block = WC_Payments_Utils::is_cart_block(); if ( ! is_product() && ! is_cart() && ! $is_cart_block ) { return; } global $product; $currency_code = get_woocommerce_currency(); $store_country = WC()->countries->get_base_country(); $billing_country = WC()->customer->get_billing_country(); $cart_total = WC()->cart->total; $product_variations = []; if ( $product ) { $get_price_fn = function ( $product ) { return $product->get_price(); }; if ( wc_tax_enabled() && $product->is_taxable() ) { if ( wc_prices_include_tax() && ( get_option( 'woocommerce_tax_display_shop' ) !== 'incl' || WC()->customer->get_is_vat_exempt() ) ) { $get_price_fn = function ( $product ) { return wc_get_price_excluding_tax( $product ); }; } elseif ( get_option( 'woocommerce_tax_display_shop' ) === 'incl' && ! WC()->customer->get_is_vat_exempt() ) { $get_price_fn = function ( $product ) { return wc_get_price_including_tax( $product ); }; } } $price = $get_price_fn( $product ); $product_variations = [ 'base_product' => [ 'amount' => WC_Payments_Utils::prepare_amount( $price, $currency_code ), 'currency' => $currency_code, ], ]; $product_price = $product_variations['base_product']['amount']; foreach ( $product->get_children() as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( $variation ) { $price = $get_price_fn( $variation ); $product_variations[ $variation_id ] = [ 'amount' => WC_Payments_Utils::prepare_amount( $price, $currency_code ), 'currency' => $currency_code, ]; $product_price = $product_variations['base_product']['amount']; } } } $enabled_upe_payment_methods = $this->gateway->get_upe_enabled_payment_method_ids(); // Filter non BNPL out of the list of payment methods. $bnpl_payment_methods = array_intersect( $enabled_upe_payment_methods, Payment_Method::BNPL_PAYMENT_METHODS ); // Filter out inactive payment methods to ensure only active BNPL methods are provided to the front-end. $payment_method_statuses = $this->gateway->get_upe_enabled_payment_method_statuses(); $capability_key_map = $this->gateway->get_payment_method_capability_key_map(); $bnpl_payment_methods = array_filter( $bnpl_payment_methods, function ( $payment_method_id ) use ( $payment_method_statuses, $capability_key_map ) { $capability_key = $capability_key_map[ $payment_method_id ] ?? null; if ( ! $capability_key ) { return false; } if ( ! array_key_exists( $capability_key, $payment_method_statuses ) ) { return false; } return 'active' === $payment_method_statuses[ $capability_key ]['status']; } ); // register the script. WC_Payments::register_script_with_dependencies( 'WCPAY_PRODUCT_DETAILS', 'dist/product-details', [ 'stripe' ] ); wp_enqueue_script( 'WCPAY_PRODUCT_DETAILS' ); // Enqueue the styles. wp_enqueue_style( 'wcpay-product-details', plugins_url( 'dist/product-details.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'dist/product-details.css' ), ); $country = empty( $billing_country ) ? $store_country : $billing_country; $script_data = [ 'productId' => 'base_product', 'productVariations' => $product_variations, 'country' => $country, 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), 'accountId' => $this->account->get_stripe_account_id(), 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ), 'paymentMethods' => array_values( $bnpl_payment_methods ), 'currencyCode' => $currency_code, 'isCart' => is_cart(), 'isCartBlock' => $is_cart_block, 'cartTotal' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ), 'nonce' => [ 'get_cart_total' => wp_create_nonce( 'wcpay-get-cart-total' ), 'is_bnpl_available' => wp_create_nonce( 'wcpay-is-bnpl-available' ), ], 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'shouldInitializePMME' => WC_Payments_Utils::is_any_bnpl_supporting_country( array_values( $bnpl_payment_methods ), $country, $currency_code ), ]; if ( $product ) { $script_data['shouldShowPMME'] = WC_Payments_Utils::is_any_bnpl_method_available( array_values( $bnpl_payment_methods ), $country, $currency_code, $product_price ); } // Create a script tag with config. wp_localize_script( 'WCPAY_PRODUCT_DETAILS', 'wcpayStripeSiteMessaging', $script_data ); // Ensure wcpayConfig is available in the page. $wcpay_config = rawurlencode( wp_json_encode( WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() ) ); wp_add_inline_script( 'WCPAY_PRODUCT_DETAILS', " var wcpayConfig = wcpayConfig || JSON.parse( decodeURIComponent( '" . esc_js( $wcpay_config ) . "' ) ); ", 'before' ); if ( ! $is_cart_block ) { return '<div id="payment-method-message"></div>'; } } } includes/class-database-cache.php 0000777 00000033256 15251156605 0013021 0 ustar 00 <?php /** * Class Database_Cache * * @package WooCommerce\Payments */ namespace WCPay; use WCPay\MultiCurrency\Interfaces\MultiCurrencyCacheInterface; use WCPay\MultiCurrency\Utils; defined( 'ABSPATH' ) || exit; // block direct access. /** * A class for caching data as an option in the database. */ class Database_Cache implements MultiCurrencyCacheInterface { const ACCOUNT_KEY = 'wcpay_account_data'; const ONBOARDING_FIELDS_DATA_KEY = 'wcpay_onboarding_fields_data'; const BUSINESS_TYPES_KEY = 'wcpay_business_types_data'; const FRAUD_SERVICES_KEY = 'wcpay_fraud_services_data'; const RECOMMENDED_PAYMENT_METHODS = 'wcpay_recommended_payment_methods'; const ADDRESS_AUTOCOMPLETE_JWT_KEY = 'wcpay_address_autocomplete_jwt'; /** * Dispute status counts cache key. * * @var string */ const DISPUTE_STATUS_COUNTS_KEY = 'wcpay_dispute_status_counts_cache'; /** * Dispute status counts cache key for test mode. * * @var string */ const DISPUTE_STATUS_COUNTS_KEY_TEST_MODE = 'wcpay_test_dispute_status_counts_cache'; /** * Active disputes cache key. * * @var string */ const ACTIVE_DISPUTES_KEY = 'wcpay_active_dispute_cache'; /** * Cache key for authorization summary data like count, total amount, etc. * * @var string */ const AUTHORIZATION_SUMMARY_KEY = 'wcpay_authorization_summary_cache'; /** * Cache key for authorization summary data like count, total amount, etc in test mode. * * @var string */ const AUTHORIZATION_SUMMARY_KEY_TEST_MODE = 'wcpay_test_authorization_summary_cache'; /** * Cache key for eligible connect incentive data. */ const CONNECT_INCENTIVE_KEY = 'wcpay_connect_incentive'; /** * Tracking info cache key. * * @var string */ const TRACKING_INFO_KEY = 'wcpay_tracking_info_cache'; /** * All cache keys. * * @var string[] */ const ALL_KEYS = [ self::ACCOUNT_KEY, self::ADDRESS_AUTOCOMPLETE_JWT_KEY, self::ONBOARDING_FIELDS_DATA_KEY, self::BUSINESS_TYPES_KEY, self::FRAUD_SERVICES_KEY, self::RECOMMENDED_PAYMENT_METHODS, self::DISPUTE_STATUS_COUNTS_KEY, self::DISPUTE_STATUS_COUNTS_KEY_TEST_MODE, self::ACTIVE_DISPUTES_KEY, self::AUTHORIZATION_SUMMARY_KEY, self::AUTHORIZATION_SUMMARY_KEY_TEST_MODE, self::CONNECT_INCENTIVE_KEY, self::TRACKING_INFO_KEY, ]; /** * Refresh disabled flag, controlling the behaviour of the get_or_add function. * * @var bool */ private $refresh_disabled; /** * In-memory cache for the duration of a single request. * * This is used to avoid multiple database reads for the same data and as a backstop in case the database write fails, * thus ensuring the cache generator is not called multiple times (which would mean multiple API calls to our platform). * * @var array */ private $in_memory_cache = []; /** * Class constructor. */ public function __construct() { $this->refresh_disabled = false; } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_action( 'action_scheduler_before_execute', [ $this, 'disable_refresh' ] ); } /** * Gets a value from cache or regenerates and adds it to the cache. * * @param string $key The options key to cache the data under. * @param callable $generator Function/callable regenerating the missing value. If null or false is returned, it will be treated as an error. * @param callable $validate_data Function/callable validating the data after it is retrieved from the cache. If it returns false, the cache will be refreshed. * @param boolean $force_refresh Regenerates the cache regardless of its state if true. * @param boolean $refreshed Is set to true if the cache has been refreshed without errors and with a non-empty value. * * @return mixed|null The cached value. NULL on failure to regenerate or validate the data. */ public function get_or_add( string $key, callable $generator, callable $validate_data, bool $force_refresh = false, bool &$refreshed = false ) { $cache_contents = $this->get_from_cache( $key ); $data = null; $old_data = null; // If the stored data is valid, prepare it for return in case we don't need to refresh. // Also initialize old_data in case of errors. if ( is_array( $cache_contents ) && array_key_exists( 'data', $cache_contents ) && $validate_data( $cache_contents['data'] ) ) { $data = $cache_contents['data']; $old_data = $data; } if ( $this->should_refresh_cache( $key, $cache_contents, $validate_data, $force_refresh ) ) { try { $data = $generator(); $errored = ( false === $data || null === $data ); } catch ( \Throwable $e ) { $errored = true; } $refreshed = ! $errored; if ( $errored ) { // Still return the old data on error and refresh the cache with it. $data = $old_data; } $this->write_to_cache( $key, $data, $errored ); } return $data; } /** * Gets a value from the cache. * * @param string $key The key to look for. * @param bool $force If set, return from the cache without checking for expiry. * * @return mixed|null The cache contents. NULL if the cache is expired or missing. */ public function get( string $key, bool $force = false ) { $cache_contents = $this->get_from_cache( $key ); if ( is_array( $cache_contents ) && array_key_exists( 'data', $cache_contents ) ) { if ( ! $force && $this->is_expired( $key, $cache_contents ) ) { return null; } return $cache_contents['data']; } return null; } /** * Stores a value in the cache. * * @param string $key The key to store the value under. * @param mixed $data The value to store. * * @return void */ public function add( string $key, $data ) { $this->write_to_cache( $key, $data, false ); } /** * Deletes a value from the cache. * * @param string $key The key to delete. * * @return void */ public function delete( string $key ) { // Remove from the in-memory cache. unset( $this->in_memory_cache[ $key ] ); // Remove from the DB cache. if ( delete_option( $key ) ) { // Clear the WP object cache to ensure the new data is fetched by other processes. wp_cache_delete( $key, 'options' ); } } /** * Delete all known cache entries. */ public function delete_all() { foreach ( self::ALL_KEYS as $key ) { $this->delete( $key ); } } /** * Hook function allowing the cache refresh to be selectively disabled in certain situations * (such as while running an Action Scheduler job). While the refresh is disabled, get_or_add * will only return the cached value and never regenerate it, even if it's expired. * * @return void */ public function disable_refresh() { $this->refresh_disabled = true; } /** * Delete all dispute-related cache entries. * This ensures both live and test mode dispute counts are refreshed. * * @return void */ public function delete_dispute_caches() { $this->delete( self::DISPUTE_STATUS_COUNTS_KEY ); $this->delete( self::DISPUTE_STATUS_COUNTS_KEY_TEST_MODE ); $this->delete( self::ACTIVE_DISPUTES_KEY ); } /** * Validates the cache contents and, given the passed params and the current application state, determines whether the cache should be refreshed. * See get_or_add. * * @param string $key The cache key. * @param mixed $cache_contents The cache contents. * @param callable $validate_data Callback used to validate the cached data by the callee. * @param boolean $force_refresh Whether a refresh should be forced. * * @return boolean True if the cache needs to be refreshed. */ private function should_refresh_cache( string $key, $cache_contents, callable $validate_data, bool $force_refresh ): bool { // Always refresh if the flag is set. if ( $force_refresh ) { return true; } // Do not refresh if doing ajax or the refresh has been disabled (running an AS job). if ( defined( 'DOING_CRON' ) || ( wp_doing_ajax() ) || $this->refresh_disabled ) { return false; } // The value of false means that there was never something cached. if ( false === $cache_contents ) { return true; } // Non-array, empty array, or missing expected fields mean corrupted data. // This also handles potential legacy data, which might have those keys missing. if ( ! is_array( $cache_contents ) || empty( $cache_contents ) || ! array_key_exists( 'data', $cache_contents ) || ! isset( $cache_contents['fetched'] ) || ! array_key_exists( 'errored', $cache_contents ) ) { return true; } // If the data is not errored but invalid, we should refresh it. if ( ! $cache_contents['errored'] && ! $validate_data( $cache_contents['data'] ) ) { return true; } // Refresh the expired data. if ( $this->is_expired( $key, $cache_contents ) ) { return true; } return false; } /** * Get the cache contents for a certain key. * * @param string $key The cache key. * * @return array|false The cache contents (array with `data`, `fetched`, and `errored` entries). * False if there is no cached data. */ private function get_from_cache( string $key ) { // Check the in-memory cache first. if ( isset( $this->in_memory_cache[ $key ] ) ) { return $this->in_memory_cache[ $key ]; } // Read from the DB cache. $data = get_option( $key ); // Store the data in the in-memory cache, including the case when there is no data cached (`false`). $this->in_memory_cache[ $key ] = $data; return $data; } /** * Wraps the data in the cache metadata and stores it. * * @param string $key The key to store the data under. * @param mixed $data The data to store. * @param boolean $errored Whether the refresh operation resulted in an error before this has been called. * * @return void */ private function write_to_cache( string $key, $data, bool $errored ) { // Add the data and expiry time to the array we're caching. $cache_contents = []; $cache_contents['data'] = $data; $cache_contents['fetched'] = time(); $cache_contents['errored'] = $errored; // Write the in-memory cache. $this->in_memory_cache[ $key ] = $cache_contents; // Create or update the DB option cache. // Note: Since we are adding the current time to the option value, WP will ALWAYS write the option because // the cache contents value is different from the current one, even if the data is the same. // A `false` result ONLY means that the DB write failed. // Yes, there is the possibility that we attempt to write the same data multiple times within the SAME second, // and we will mistakenly think that the DB write failed. We are OK with this false positive, // since the actual data is the same. $result = update_option( $key, $cache_contents, 'no' ); if ( false !== $result ) { // If the DB cache write succeeded, clear the WP object cache to ensure the new data is fetched by other processes. wp_cache_delete( $key, 'options' ); } } /** * Checks if the cache contents are expired. * * @param string $key The cache key. * @param array $cache_contents The cache contents. * * @return boolean True if the contents are expired. False otherwise. */ private function is_expired( string $key, array $cache_contents ): bool { $ttl = $this->get_ttl( $key, $cache_contents ); $expires = $cache_contents['fetched'] + $ttl; $now = time(); return $expires < $now; } /** * Given the key and the cache contents, and based on the application state, determines the cache TTL. * * @param string $key The cache key. * @param array $cache_contents The cache contents. * * @return integer The cache TTL. */ private function get_ttl( string $key, array $cache_contents ): int { switch ( $key ) { case self::ACCOUNT_KEY: if ( is_admin() ) { // Fetches triggered from the admin panel should be more frequent. if ( $cache_contents['errored'] ) { // Attempt to refresh the data quickly if the last fetch was an error. $ttl = 2 * MINUTE_IN_SECONDS; } else { // If the data was fetched successfully, cache it for 2h. $ttl = 2 * HOUR_IN_SECONDS; } } else { // For performance reasons, non-admin requests should use cached data for longer (24h). $ttl = DAY_IN_SECONDS; } break; case self::CURRENCIES_KEY: if ( defined( 'DOING_CRON' ) || is_admin() || Utils::is_admin_api_request() ) { // Fetches triggered from the admin panel should be more frequent. if ( $cache_contents['errored'] ) { // Attempt to refresh the data quickly if the last fetch was an error. $ttl = 2 * MINUTE_IN_SECONDS; } else { // If the data was fetched successfully, cache it for 3h. $ttl = 3 * HOUR_IN_SECONDS; } } else { // For performance reasons, non-admin requests should use cached data for longer (12h). $ttl = 12 * HOUR_IN_SECONDS; } break; case self::BUSINESS_TYPES_KEY: case self::ONBOARDING_FIELDS_DATA_KEY: // Cache these for a week. $ttl = WEEK_IN_SECONDS; break; case self::CONNECT_INCENTIVE_KEY: $ttl = $cache_contents['data']['ttl'] ?? HOUR_IN_SECONDS * 6; break; case self::CONNECT_INCENTIVE_KEY . '_has_orders': // If the store has orders, cache for 90 days since it won't change. // If no orders, cache for an hour to check again soon. $ttl = $cache_contents['data'] ? DAY_IN_SECONDS * 90 : HOUR_IN_SECONDS; break; case self::TRACKING_INFO_KEY: $ttl = $cache_contents['errored'] ? 2 * MINUTE_IN_SECONDS : MONTH_IN_SECONDS; break; case self::ADDRESS_AUTOCOMPLETE_JWT_KEY: $ttl = 12 * HOUR_IN_SECONDS; break; default: // Default to 24h. $ttl = DAY_IN_SECONDS; break; } return apply_filters( 'wcpay_database_cache_ttl', $ttl, $key, $cache_contents ); } } includes/class-wc-payments-session-service.php 0000777 00000012312 15251156605 0015570 0 ustar 00 <?php /** * WC_Payments_Session_Service class * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Exceptions\API_Exception; /** * Handles sessions and session details. */ class WC_Payments_Session_Service { const SESSION_STORE_ID_OPTION = 'wcpay_session_store_id'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * Constructor for WC_Payments_Session_Service. * * @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client. */ public function __construct( WC_Payments_API_Client $payments_api_client ) { $this->payments_api_client = $payments_api_client; } /** * Checks if the user has just logged in. * * The user just logged in if the session cookie contains a different customer ID than the one in the session. * * @return boolean True if the user has just logged in, false in any other case. */ public function user_just_logged_in(): bool { if ( ! get_current_user_id() ) { return false; } WC()->initialize_session(); $session_handler = WC()->session; // The Store API SessionHandler (used by WooPay) doesn't provide this method. if ( ! method_exists( $session_handler, 'get_session_cookie' ) ) { return false; } $cookie = $session_handler->get_session_cookie(); if ( ! $cookie ) { return false; } $cookie_customer_id = $cookie[0]; return $session_handler->get_customer_id() !== $cookie_customer_id; } /** * Get the Sift session ID for the current browsing session. * * @return string|null The Sift session ID or null if it can't be determined. */ public function get_sift_session_id(): ?string { if ( $this->user_just_logged_in() ) { return $this->get_cookie_session_id(); } if ( is_a( WC()->session, 'WC_Session' ) ) { return $this->generate_session_id( $this->get_store_id(), (string) WC()->session->get_customer_id() ); } return null; // We do not have a valid session for the current process. } /** * Link a customer with the current browsing session, for tracking purposes. * * @param string $customer_id Stripe customer ID. * * @return array An array, containing a `success` flag. * * @throws API_Exception If an error occurs. */ public function link_current_session_to_customer( string $customer_id ): array { return $this->payments_api_client->link_session_to_customer( $this->get_sift_session_id(), $customer_id ); } /** * Get the session ID used until now for the current browsing session. * * It looks for the logged in user ID stored in the session cookie, and uses that to generate a session ID. * * @return string|null Session ID, or null if unknown. */ public function get_cookie_session_id(): ?string { $session_handler = WC()->session; if ( ! $session_handler ) { return null; } // The Store API SessionHandler (used by WooPay) doesn't provide this method. if ( ! method_exists( $session_handler, 'get_session_cookie' ) ) { return null; } $cookie = $session_handler->get_session_cookie(); if ( ! $cookie ) { return null; } $cookie_customer_id = $cookie[0]; return $this->generate_session_id( $this->get_store_id(), (string) $cookie_customer_id ); } /** * Get the store ID for use in sessions. * * This is used to consistently identify the store in WooPayments sessions. * If it doesn't exist, it is generated randomly and stored in the database. * * @return string The store ID or empty string if it can't be determined. */ public function get_store_id(): string { // We will use a stored random store ID. $store_id = get_option( self::SESSION_STORE_ID_OPTION, false ); if ( ! $store_id ) { $store_id = $this->generate_store_id(); update_option( self::SESSION_STORE_ID_OPTION, $store_id ); } return $store_id; } /** * Generate a random store ID. * * The generated ID is case-sensitive and contains 32 characters. * * @return string The generated store ID. */ private function generate_store_id(): string { // Prefix it with 'st_' (from store) to make it easier to identify. $prefix = 'st_'; // We will generate 32 characters in total, including the prefix length. $length = 32 - strlen( $prefix ); // We will use alphanumerical characters. $include_chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; // Add some special characters, not all. Play it safe. // See the Sift restrictions for $user_id. // @link https://sift.com/developers/docs/curl/events-api/fields. $include_chars .= '-$:.^!'; // Finally, shuffle them for extra randomness. $include_chars = str_shuffle( $include_chars ); $char_length = strlen( $include_chars ); $random_string = ''; for ( $i = 0; $i < $length; $i++ ) { $random_string .= $include_chars [ wp_rand( 0, $char_length - 1 ) ]; } return $prefix . $random_string; } /** * Generate a session ID based on the store ID and the user ID. * * @param string $store_id The session store ID. * @param string $user_id The user ID. * * @return string */ private function generate_session_id( string $store_id, string $user_id ): string { return $store_id . '_' . $user_id; } } includes/class-wc-payments-vat-redirect-service.php 0000777 00000002451 15251156605 0016501 0 ustar 00 <?php /** * WooCommerce Payments VAT Redirect Service. * * @package WooCommerce\Payments */ namespace WCPay; defined( 'ABSPATH' ) || exit; // block direct access. /** * Service class for handling VAT details redirect functionality. * * @todo This is a temporary solution that will be replaced by a dedicated VAT settings section. * Remove this class when the permanent solution is implemented. */ class WC_Payments_VAT_Redirect_Service { /** * Initialize hooks. * * @return void */ public function init_hooks(): void { add_action( 'template_redirect', [ $this, 'maybe_redirect' ], 1 ); } /** * Handles redirection to VAT details modal if needed. * * @return void */ public function maybe_redirect(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['woopayments-vat-details-redirect'] ) ) { return; } // phpcs:enable WordPress.Security.NonceVerification.Recommended // skip REST API calls. if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return; } // skip AJAX requests. if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) { return; } wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments&woopayments-vat-details-modal=true' ) ); exit; } } includes/class-logger.php 0000777 00000010374 15251156605 0011467 0 ustar 00 <?php /** * Class Logger * * @package WooCommerce\Payments */ namespace WCPay; use Throwable; use WCPay\Internal\Logger as InternalLogger; defined( 'ABSPATH' ) || exit; // block direct access. /** * A wrapper class for interacting with WC_Logger. */ class Logger { /** * Add a log entry. * * Note that this depends on WC_Payments gateway property to be initialized as * we need this to access the plugins debug setting to figure out if the setting * is turned on. * * @param string $message Log message. * * @param string $level One of the following: * 'emergency': System is unusable. * 'alert': Action must be taken immediately. * 'critical': Critical conditions. * 'error': Error conditions. * 'warning': Warning conditions. * 'notice': Normal but significant condition. * 'info': Informational messages. * 'debug': Debug-level messages. * @param array<string, mixed> $context Context data. */ public static function log( $message, $level = 'info', $context = [] ) { wcpay_get_container()->get( InternalLogger::class )->log( $message, $level, $context ); } /** * Checks if the gateway setting logging toggle is enabled. * * @return bool Depending on the enable_logging setting. */ public static function can_log() { return wcpay_get_container()->get( InternalLogger::class )->can_log(); } /** * Creates a log entry of type emergency * * @param string $message To send to the log file. * @param array<string, mixed> $context Context data. */ public static function emergency( $message, $context = [] ) { self::log( $message, 'emergency', $context ); } /** * Creates a log entry of type alert * * @param string $message To send to the log file. * @param array<string, mixed> $context Context data. */ public static function alert( $message, $context = [] ) { self::log( $message, 'alert', $context ); } /** * Creates a log entry of type critical * * @param string $message To send to the log file. * @param array<string, mixed> $context Context data. */ public static function critical( $message, $context = [] ) { self::log( $message, 'critical', $context ); } /** * Creates a log entry of type error * * @param string $message to send to the log file. * @param array<string, mixed> $context context data. */ public static function error( $message, $context = [] ) { self::log( $message, 'error', $context ); } /** * Creates a log entry for exception * * @param string $message Message to prepend to an exception. * @param Throwable $e Exception to log. * @param array<string, mixed> $context Context data. */ public static function exception( $message, $e, $context = [] ) { self::error( $message . ' Exception: ' . $e->getMessage(), array_merge( [ 'exception' => get_class( $e ), 'code' => $e->getCode(), 'trace' => $e->getTraceAsString(), ], $context ) ); } /** * Creates a log entry of type warning * * @param string $message To send to the log file. * @param array<string, mixed> $context Context data. */ public static function warning( $message, $context = [] ) { self::log( $message, 'warning', $context ); } /** * Creates a log entry of type notice * * @param string $message To send to the log file. * @param array<string, mixed> $context Context data. */ public static function notice( $message, $context = [] ) { self::log( $message, 'notice', $context ); } /** * Creates a log entry of type info * * @param string $message To send to the log file. * @param array<string, mixed> $context Context data. */ public static function info( $message, $context = [] ) { self::log( $message, 'info', $context ); } /** * Creates a log entry of type debug * * @param string $message To send to the log file. * @param array<string, mixed> $context Context data. */ public static function debug( $message, $context = [] ) { self::log( $message, 'debug', $context ); } } includes/woopay-user/class-woopay-save-user.php 0000777 00000007735 15251156605 0015737 0 ustar 00 <?php /** * Class WooPaySaveUser * * @package WooCommerce\Payments\WooPay */ defined( 'ABSPATH' ) || exit; use WCPay\WooPay\WooPay_Session; use WCPay\WooPay\WooPay_Utilities; /** * Class that adds a section to save new user for woopay on the frontend. */ class WooPay_Save_User { /** * WooPay utilities. * * @var WooPay_Utilities */ protected $woopay_util; /** * Constructor. */ public function __construct() { $this->woopay_util = new WooPay_Utilities(); add_action( 'wp_enqueue_scripts', [ $this, 'register_checkout_page_scripts' ] ); add_filter( 'wcpay_metadata_from_order', [ $this, 'maybe_add_userdata_to_metadata' ], 10, 2 ); add_action( 'woocommerce_payment_complete', [ $this, 'maybe_clear_session_key' ], 10, 2 ); } /** * Load scripts and styles for checkout page. */ public function register_checkout_page_scripts() { if ( ! is_checkout() && ! has_block( 'woocommerce/checkout' ) ) { return; } // Don't enqueue checkout page scripts when WCPay isn't available. $gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( ! isset( $gateways['woocommerce_payments'] ) ) { return; } if ( ! $this->woopay_util->is_country_available() ) { return; } $style_url = plugins_url( 'dist/woopay.css', WCPAY_PLUGIN_FILE ); WC_Payments_Utils::enqueue_style( 'WCPAY_WOOPAY', $style_url, [], \WC_Payments::get_file_version( 'dist/woopay.css' ), 'all' ); WC_Payments::register_script_with_dependencies( 'WCPAY_WOOPAY', 'dist/woopay' ); $account_data = WC_Payments::get_account_service()->get_cached_account_data(); wp_localize_script( 'WCPAY_WOOPAY', 'woopayCheckout', [ 'PRE_CHECK_SAVE_MY_INFO' => isset( $account_data['pre_check_save_my_info'] ) ? $account_data['pre_check_save_my_info'] : false, ] ); wp_enqueue_script( 'WCPAY_WOOPAY' ); } /** * Checks if the user chose to save their data for woopay and adds appropriate metadata. * * @param array $metadata Metadata to be saved. * @param \WC_Order $order Order object. * * @return array */ public function maybe_add_userdata_to_metadata( $metadata, $order ) { $should_save_platform_customer = $this->woopay_util->should_save_platform_customer(); $woopay_phone = $this->woopay_util->get_woopay_phone(); if ( $should_save_platform_customer && $woopay_phone ) { $woopay_source_url = $this->woopay_util->get_woopay_source_url(); $woopay_is_blocks = $this->woopay_util->get_woopay_is_blocks(); $woopay_viewport = $this->woopay_util->get_woopay_viewport(); // Add the metadata. $metadata['platform_checkout_primary_first_name'] = wc_clean( $order->get_billing_first_name() ); $metadata['platform_checkout_primary_last_name'] = wc_clean( $order->get_billing_last_name() ); $metadata['platform_checkout_primary_phone'] = wc_clean( $order->get_billing_phone() ); $metadata['platform_checkout_primary_company'] = wc_clean( $order->get_billing_company() ); $metadata['platform_checkout_secondary_first_name'] = wc_clean( $order->get_shipping_first_name() ); $metadata['platform_checkout_secondary_last_name'] = wc_clean( $order->get_shipping_last_name() ); $metadata['platform_checkout_secondary_phone'] = wc_clean( $order->get_shipping_phone() ); $metadata['platform_checkout_secondary_company'] = wc_clean( $order->get_shipping_company() ); $metadata['platform_checkout_phone'] = $woopay_phone; $metadata['platform_checkout_source_url'] = $woopay_source_url; $metadata['platform_checkout_is_blocks'] = $woopay_is_blocks; $metadata['platform_checkout_viewport'] = $woopay_viewport; } return $metadata; } /** * Clears if woopay user data is set. * * @return void */ public function maybe_clear_session_key() { $session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY ); if ( ! empty( $session_data ) ) { WC()->session->__unset( WooPay_Session::WOOPAY_SESSION_KEY ); } } } includes/class-wc-payments-onboarding-service.php 0000777 00000157507 15251156605 0016247 0 ustar 00 <?php /** * Class WC_Payments_Onboarding_Service * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use Automattic\WooCommerce\Admin\Notes\DataStore; use Automattic\WooCommerce\Admin\Notes\Note; use WCPay\Database_Cache; use WCPay\Exceptions\API_Exception; use WCPay\Logger; /** * Class handling onboarding related business logic. */ class WC_Payments_Onboarding_Service { const TEST_MODE_OPTION = 'wcpay_onboarding_test_mode'; const ONBOARDING_CONNECTION_SUCCESS_MODAL_OPTION = 'wcpay_connection_success_modal_dismissed'; const ONBOARDING_INIT_IN_PROGRESS_TRANSIENT = 'wcpay_onboarding_init_in_progress'; // Onboarding flow sources. // We use these to identify the originating place for the current onboarding flow. // This should be very sticky as opposed to the `from` value which is meant to represent the immediately previous step. const SOURCE_WCADMIN_PAYMENT_TASK = 'wcadmin-payment-task'; const SOURCE_WCADMIN_SETTINGS_PAGE = 'wcadmin-settings-page'; const SOURCE_WCADMIN_NOX_IN_CONTEXT = 'wcadmin-nox-in-context'; const SOURCE_WCADMIN_INCENTIVE_PAGE = 'wcadmin-incentive-page'; const SOURCE_WCPAY_CONNECT_PAGE = 'wcpay-connect-page'; const SOURCE_WCPAY_OVERVIEW_PAGE = 'wcpay-overview-page'; const SOURCE_WCPAY_PAYOUTS_PAGE = 'wcpay-payouts-page'; const SOURCE_WCPAY_RESET_ACCOUNT = 'wcpay-reset-account'; const SOURCE_WCPAY_SETUP_LIVE_PAYMENTS = 'wcpay-setup-live-payments'; const SOURCE_WCPAY_FINISH_SETUP_TASK = 'wcpay-finish-setup-task'; const SOURCE_WCPAY_UPDATE_BUSINESS_DETAILS_TASK = 'wcpay-update-business-details-task'; const SOURCE_WCPAY_PO_BANK_ACCOUNT_TASK = 'wcpay-po-bank-account-task'; const SOURCE_WCPAY_RECONNECT_WPCOM_TASK = 'wcpay-reconnect-wpcom-task'; const SOURCE_WCPAY_GO_LIVE_TASK = 'wcpay-go-live-task'; const SOURCE_WCPAY_FINISH_SETUP_TOOL = 'wcpay-finish-setup-tool'; const SOURCE_WCPAY_PAYOUT_FAILURE_NOTICE = 'wcpay-payout-failure-notice'; const SOURCE_WCPAY_ACCOUNT_DETAILS = 'wcpay-account-details'; const SOURCE_UNKNOWN = 'unknown'; // Values for the `from` GET param to indicate what was the immediately previous step. // Woo core places. const FROM_WCADMIN_PAYMENTS_TASK = 'WCADMIN_PAYMENT_TASK'; const FROM_WCADMIN_PAYMENTS_SETTINGS = 'WCADMIN_PAYMENT_SETTINGS'; const FROM_WCADMIN_NOX_IN_CONTEXT = 'WCADMIN_NOX_IN_CONTEXT'; const FROM_WCADMIN_INCENTIVE = 'WCADMIN_PAYMENT_INCENTIVE'; // WooPayments places. const FROM_CONNECT_PAGE = 'WCPAY_CONNECT'; const FROM_OVERVIEW_PAGE = 'WCPAY_OVERVIEW'; const FROM_ACCOUNT_DETAILS = 'WCPAY_ACCOUNT_DETAILS'; const FROM_ONBOARDING_WIZARD = 'WCPAY_ONBOARDING_WIZARD'; const FROM_ONBOARDING_KYC = 'WCPAY_ONBOARDING_KYC'; // The embedded Stripe KYC step/page. const FROM_SETTINGS = 'WCPAY_SETTINGS'; const FROM_PAYOUTS = 'WCPAY_PAYOUTS'; const FROM_TEST_TO_LIVE = 'WCPAY_TEST_TO_LIVE'; const FROM_GO_LIVE_TASK = 'WCPAY_GO_LIVE_TASK'; const FROM_RESET_ACCOUNT = 'WCPAY_RESET_ACCOUNT'; const FROM_PLUGIN_ACTIVATION = 'WCPAY_ACTIVE'; // External places. const FROM_WPCOM = 'WPCOM'; const FROM_WPCOM_CONNECTION = 'WPCOM_CONNECTION'; const FROM_STRIPE = 'STRIPE'; const FROM_STRIPE_EMBEDDED = 'STRIPE_EMBEDDED'; const FROM_REFERRAL = 'REFERRAL'; const TRACKS_EVENT_ONBOARDING_RESET = 'wcpay_onboarding_flow_reset'; const TRACKS_EVENT_TEST_DRIVE_ACCOUNT_DISABLE = 'wcpay_onboarding_test_account_disable'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * Cache util for managing onboarding data. * * @var Database_Cache */ private $database_cache; /** * Session service. * * @var WC_Payments_Session_Service instance for working with session information */ private $session_service; /** * Class constructor * * @param WC_Payments_API_Client $payments_api_client Payments API client. * @param Database_Cache $database_cache Database cache util. * @param WC_Payments_Session_Service $session_service Session service. */ public function __construct( WC_Payments_API_Client $payments_api_client, Database_Cache $database_cache, WC_Payments_Session_Service $session_service ) { $this->payments_api_client = $payments_api_client; $this->database_cache = $database_cache; $this->session_service = $session_service; } /** * Initialise class hooks. * * @return void */ public function init_hooks() { add_filter( 'admin_body_class', [ $this, 'add_admin_body_classes' ] ); add_filter( 'wc_payments_get_onboarding_data_args', [ $this, 'maybe_add_test_drive_settings_to_new_account_request' ] ); } /** * Retrieve the fields data to use in the onboarding form. * * The data is retrieved from the server and is cached. If we can't retrieve, we will use whatever data we have. * * @param string $locale The locale to use to i18n the data. * @param bool $__deprecated Force-refresh flag, deprecated. * @return ?array Fields data, or NULL if failed to retrieve. */ public function get_fields_data( string $locale = '', bool $__deprecated = false ): ?array { if ( false !== $__deprecated ) { wc_deprecated_argument( __CLASS__ . '::' . __METHOD__, '10.5.0', 'Force-refresh argument is deprecated.' ); } // If we don't have a server connection, return what data we currently have, regardless of expiry. if ( ! $this->payments_api_client->is_server_connected() ) { return $this->database_cache->get( Database_Cache::ONBOARDING_FIELDS_DATA_KEY, true ); } return $this->database_cache->get_or_add( Database_Cache::ONBOARDING_FIELDS_DATA_KEY, function () use ( $locale ) { try { // We will use the language for the current user (defaults to the site language). $fields_data = $this->payments_api_client->get_onboarding_fields_data( $locale ); } catch ( API_Exception $e ) { // Return NULL to signal retrieval error. return null; } // Store the locale, so if a different one is requested, we can invalidate the cache. $fields_data['__locale'] = $locale; return $fields_data; }, function ( $data ) use ( $locale ) { // The locale used to be part of a dynamic key. If it is not set, the data is old & invalid. return ( is_array( $data ) && isset( $data['__locale'] ) && $data['__locale'] === $locale ); } ); } /** * Retrieve and cache the account recommended payment methods list. * * @param string $country_code The account's business location country code. Provide a 2-letter ISO country code. * @param string $locale Optional. The locale to use to i18n the data. * * @return ?array The recommended payment methods list. * NULL on retrieval or validation error. */ public function get_recommended_payment_methods( string $country_code, string $locale = '' ): ?array { $cached_data = \WC_Payments::get_database_cache()->get_or_add( Database_Cache::RECOMMENDED_PAYMENT_METHODS, function () use ( $country_code, $locale ) { try { $payment_methods = $this->payments_api_client->get_recommended_payment_methods( $country_code, $locale ); // Indicate that the cached value is specific for the given locale and country code. return [ 'payment_methods' => $payment_methods, '__locale' => $locale, '__country_code' => $country_code, ]; } catch ( API_Exception $e ) { // Return NULL to signal retrieval error. return null; } }, function ( $data ) use ( $locale, $country_code ) { // The locale and country code used to be part of a dynamic key. // If either is not set, the data is old & invalid. return ( is_array( $data ) && isset( $data['payment_methods'] ) && isset( $data['__locale'] ) && isset( $data['__country_code'] ) && $data['__locale'] === $locale && $data['__country_code'] === $country_code ); } ); return $cached_data['payment_methods'] ?? null; } /** * Get the onboarding capabilities from the request. * * The capabilities are expected to be passed as an array of capabilities keyed by the capability ID and * with boolean values. If the value is true, the capability is requested when the account is created. * * @return array The standardized capabilities that were passed in the request. * Empty array if no capabilities were passed or none were valid. */ public function get_capabilities_from_request(): array { $capabilities = []; if ( empty( $_REQUEST['capabilities'] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended return $capabilities; } // Try to extract the capabilities. // They might be already decoded or not, so we need to handle both cases. // We expect them to be an array. // We disable the warning because we have our own sanitization and validation. // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $capabilities = wp_unslash( $_REQUEST['capabilities'] ); if ( ! is_array( $capabilities ) ) { $capabilities = json_decode( $capabilities, true ) ?? []; } if ( empty( $capabilities ) ) { return []; } // Sanitize and validate. $capabilities = array_combine( array_map( function ( $key ) { // Keep numeric keys as integers so we can remove them later. if ( is_numeric( $key ) ) { return intval( $key ); } return sanitize_text_field( $key ); }, array_keys( $capabilities ) ), array_map( function ( $value ) { return filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); }, $capabilities ) ); // Filter out any invalid entries. $capabilities = array_filter( $capabilities, function ( $value, $key ) { return is_string( $key ) && is_bool( $value ); }, ARRAY_FILTER_USE_BOTH ); return $capabilities; } /** * Checks if the WooPay capabilities should be enabled by default based on the capabilities list. * * @param bool $default_value Whether WooPay should be enabled by default. * @param array $capabilities The capabilities list. * * @return bool Whether WooPay should be enabled by default. */ public function should_enable_woopay( bool $default_value, array $capabilities ): bool { // The capabilities has `_payments` suffix. $woopay_capability = 'woopay_payments'; // If the capabilities list is empty, we should return the default value. if ( empty( $capabilities ) ) { return $default_value; } // Return the value from the capabilities list. return ! empty( $capabilities[ $woopay_capability ] ); } /** * Retrieve the embedded KYC session and handle initial account creation (if necessary). * * Will return the session key used to initialise the embedded onboarding session. * * @param array $self_assessment_data Self assessment data. * @param array $capabilities Optional. List keyed by capabilities IDs (payment methods) with boolean values * indicating whether the capability should be requested when the account is created * and enabled in the settings. * * @return array Session data. * * @throws API_Exception|Exception */ public function create_embedded_kyc_session( array $self_assessment_data, array $capabilities = [] ): array { if ( ! $this->payments_api_client->is_server_connected() ) { WC_Payments_Utils::log_to_wc( 'Failed to create embedded KYC session: Jetpack connection not available.' ); return []; } if ( $this->is_onboarding_init_in_progress() ) { Logger::warning( 'Duplicate onboarding attempt detected.' ); // We can't allow multiple onboarding initializations to happen at the same time. throw new Exception( __( 'Onboarding initialization is already in progress. Please wait for it to finish.', 'woocommerce-payments' ) ); } $this->set_onboarding_init_in_progress(); $setup_mode = WC_Payments::mode()->is_live() ? 'live' : 'test'; // Make sure the onboarding test mode DB flag is set. self::set_test_mode( 'live' !== $setup_mode ); $site_data = [ 'site_username' => wp_get_current_user()->user_login, 'site_locale' => get_locale(), ]; $user_data = $this->get_onboarding_user_data(); $account_data = $this->get_account_data( $setup_mode, $self_assessment_data, $capabilities ); $actioned_notes = self::get_actioned_notes(); /** * ================== * Enforces the update of payment methods to 'enabled' based on the capabilities * provided during the NOX onboarding process. * * @see self::update_enabled_payment_methods_ids * ================== */ $gateway = WC_Payments::get_gateway(); // Activate enabled Payment Methods IDs. if ( ! empty( $capabilities ) ) { $this->update_enabled_payment_methods_ids( $gateway, $capabilities ); } try { $account_session = $this->payments_api_client->initialize_onboarding_embedded_kyc( 'live' === $setup_mode, $site_data, WC_Payments_Utils::array_filter_recursive( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. WC_Payments_Utils::array_filter_recursive( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. $actioned_notes, $this->get_referral_code() ); } catch ( API_Exception $e ) { $this->clear_onboarding_init_in_progress(); WC_Payments_Utils::log_to_wc( 'Failed to create embedded KYC session: ' . $e->getMessage() ); // If we fail to create the session, return an empty array. return []; } $this->clear_onboarding_init_in_progress(); // Set the embedded KYC in progress flag. $this->set_embedded_kyc_in_progress(); // Remember if we should enable WooPay by default. set_transient( WC_Payments_Account::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT, filter_var( $account_session['woopay_enabled_by_default'] ?? false, FILTER_VALIDATE_BOOLEAN ), DAY_IN_SECONDS ); // If we have a new account, clear the account cache to force a refresh. if ( ! empty( $account_session['account_created'] ) ) { WC_Payments::get_account_service()->clear_cache(); } return [ 'clientSecret' => $account_session['client_secret'] ?? '', 'expiresAt' => $account_session['expires_at'] ?? 0, 'accountId' => $account_session['account_id'] ?? '', 'isLive' => $account_session['is_live'] ?? false, 'accountCreated' => $account_session['account_created'] ?? false, 'publishableKey' => $account_session['publishable_key'] ?? '', ]; } /** * Finalize the embedded KYC session. * * @param string $locale The locale to use to i18n the data. * @param string $source The source of the onboarding flow. * @param array $actioned_notes The actioned notes for this onboarding. * * @return array Containing the following keys: success, account_id, mode. * * @throws API_Exception */ public function finalize_embedded_kyc( string $locale, string $source, array $actioned_notes ): array { if ( ! $this->payments_api_client->is_server_connected() ) { WC_Payments_Utils::log_to_wc( 'Failed to finalize embedded KYC: Jetpack connection not available.' ); return [ 'success' => false, ]; } $result = $this->payments_api_client->finalize_onboarding_embedded_kyc( $locale, $source, $actioned_notes ); $success = $result['success'] ?? false; $details_submitted = $result['details_submitted'] ?? false; if ( ! $result || ! $success ) { throw new API_Exception( __( 'Failed to finalize onboarding session.', 'woocommerce-payments' ), 'wcpay-onboarding-finalize-error', 400 ); } // Clear the embedded KYC in progress option, since the onboarding flow is now complete. $this->clear_embedded_kyc_in_progress(); // Clear the account cache to make sure the account data is fresh // and not depend on webhooks that might not have been received yet. WC_Payments::get_account_service()->clear_cache(); return [ 'success' => $success, 'details_submitted' => $details_submitted, 'account_id' => $result['account_id'] ?? '', 'mode' => $result['mode'], 'promotion_id' => $result['promotion_id'] ?? null, ]; } /** * Gets and caches the business types per country from the server. * * @return array|bool Business types, or false if failed to retrieve. */ public function get_cached_business_types() { if ( ! $this->payments_api_client->is_server_connected() ) { return []; } $business_types = $this->database_cache->get_or_add( Database_Cache::BUSINESS_TYPES_KEY, function () { try { $business_types = $this->payments_api_client->get_onboarding_business_types(); } catch ( API_Exception $e ) { // Return false to signal retrieval error. return false; } if ( ! $this->is_valid_cached_business_types( $business_types ) ) { return false; } return $business_types; }, [ $this, 'is_valid_cached_business_types' ] ); if ( null === $business_types ) { return false; } return $business_types; } /** * Check whether an onboarding initialization is in progress. * * This only relates to the initial account creation, not the full KYC flow. * * @return bool Whether an onboarding flow is in progress. */ public function is_onboarding_init_in_progress(): bool { return filter_var( get_transient( self::ONBOARDING_INIT_IN_PROGRESS_TRANSIENT ), FILTER_VALIDATE_BOOLEAN ); } /** * Mark the onboarding initialization as in progress. * * This only relates to the initial account creation, not the full KYC flow. * * @return void */ public function set_onboarding_init_in_progress(): void { set_transient( self::ONBOARDING_INIT_IN_PROGRESS_TRANSIENT, 'yes', 3 * MINUTE_IN_SECONDS ); } /** * Clear the onboarding initialization in progress transient. * * @return void */ public function clear_onboarding_init_in_progress(): void { delete_transient( self::ONBOARDING_INIT_IN_PROGRESS_TRANSIENT ); } /** * Check whether the business types fetched from the cache are valid. * * @param array|bool|string $business_types The business types returned from the cache. * * @return bool */ public function is_valid_cached_business_types( $business_types ): bool { if ( null === $business_types || false === $business_types ) { return false; } // Non-array values are not expected, and we expect a non-empty array. if ( ! is_array( $business_types ) || empty( $business_types ) ) { return false; } return true; } /** * Adds body classes to the main wp-admin wrapper. * * @param string $classes Space separated string of class names. * * @return string Classes to add to the body. */ public function add_admin_body_classes( string $classes = '' ): string { // Onboarding needs to hide wp-admin navigation and masterbar while JS loads. // This class will be removed by the onboarding component. if ( isset( $_GET['path'] ) && '/payments/onboarding' === $_GET['path'] ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended $classes .= ' woocommerce-admin-is-loading'; } return $classes; } /** * Get account data for onboarding from self assessment data. * * @param string $setup_mode Setup mode. * @param array $self_assessment_data Self assessment data. * @param array $capabilities Optional. List keyed by capabilities IDs (payment methods) with boolean values. * If the value is true, the capability is requested when the account is created. * If the value is false, the capability is not requested when the account is created. * * @return array Account data. */ public function get_account_data( string $setup_mode, array $self_assessment_data, array $capabilities = [] ): array { $home_url = get_home_url(); // If the site is running on localhost, use a bogus URL. This is to avoid Stripe's errors. // wp_http_validate_url does not check that, unfortunately. $home_is_localhost = 'localhost' === wp_parse_url( $home_url, PHP_URL_HOST ); $fallback_url = ( 'live' !== $setup_mode || $home_is_localhost ) ? 'https://wcpay.test' : null; $current_user = get_userdata( get_current_user_id() ); // The general account data. $account_data = [ 'setup_mode' => $setup_mode, // We use the store base country to create a customized account. 'country' => WC()->countries->get_base_country() ?? null, 'url' => ! $home_is_localhost && wp_http_validate_url( $home_url ) ? $home_url : $fallback_url, 'business_name' => get_bloginfo( 'name' ), ]; foreach ( $capabilities as $capability => $should_request ) { // Remove the `_payments` suffix from the capability, if present. if ( strpos( $capability, '_payments' ) === strlen( $capability ) - 9 ) { $capability = str_replace( '_payments', '', $capability ); } // Skip the special 'apple_google' because it is not a payment method. // Skip the 'woopay' because it is automatically handled by the API. if ( 'apple_google' === $capability || 'woopay' === $capability ) { continue; } if ( 'card' === $capability ) { // Card is always requested. $account_data['capabilities']['card_payments'] = [ 'requested' => 'true' ]; // When requesting card, we also need to request transfers. // The platform should handle this automatically, but it is best to be thorough. $account_data['capabilities']['transfers'] = [ 'requested' => 'true' ]; continue; } // We only request, not unrequest capabilities. if ( $should_request ) { $account_data['capabilities'][ $capability . '_payments' ] = [ 'requested' => 'true' ]; } } if ( ! empty( $self_assessment_data ) ) { $business_type = $self_assessment_data['business_type'] ?? null; $account_data = WC_Payments_Utils::array_merge_recursive_distinct( $account_data, [ // Overwrite the country if the merchant chose a different one than the Woo base location. 'country' => $self_assessment_data['country'] ?? null, 'email' => $self_assessment_data['email'] ?? null, 'business_name' => $self_assessment_data['business_name'] ?? null, 'url' => $self_assessment_data['site'] ?? null, 'mcc' => $self_assessment_data['mcc'] ?? null, 'business_type' => $business_type, 'company' => [ 'structure' => 'company' === $business_type ? ( $self_assessment_data['company']['structure'] ?? null ) : null, ], 'individual' => [ 'first_name' => $self_assessment_data['individual']['first_name'] ?? null, 'last_name' => $self_assessment_data['individual']['last_name'] ?? null, 'phone' => $self_assessment_data['phone'] ?? null, ], ] ); } elseif ( 'test_drive' === $setup_mode ) { $account_data = WC_Payments_Utils::array_merge_recursive_distinct( $account_data, [ 'individual' => [ 'first_name' => $current_user->first_name ?? null, 'last_name' => $current_user->last_name ?? null, ], ] ); } elseif ( 'test' === $setup_mode ) { $account_data = WC_Payments_Utils::array_merge_recursive_distinct( $account_data, [ 'business_type' => 'individual', 'mcc' => '5734', 'individual' => [ 'first_name' => $current_user->first_name ?? null, 'last_name' => $current_user->last_name ?? null, ], ] ); } return $account_data; } /** * Get user data to send to the onboarding flow. * * @return array The user data. */ public function get_onboarding_user_data(): array { return [ 'user_id' => get_current_user_id(), 'sift_session_id' => $this->session_service->get_sift_session_id(), 'ip_address' => \WC_Geolocation::get_ip_address(), 'browser' => [ 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', 'accept_language' => isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '', 'content_language' => empty( get_user_locale() ) ? 'en-US' : str_replace( '_', '-', get_user_locale() ), ], 'referer' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', 'onboarding_source' => self::get_source(), ]; } /** * Initialize a test-drive account. * * Note: This is a subset of the WC_Payments_Account::maybe_handle_onboarding method. * * @param string $country The country code to use for the account. * This is a ISO 3166-1 alpha-2 country code. * @param array $capabilities Optional. List keyed by capabilities IDs (payment methods) with boolean values * indicating whether the capability should be requested when the account is created * and enabled in the settings. * * @return bool Whether the account was created. * @throws API_Exception When the API request fails. * @throws Exception When an onboarding initialization is already in progress. */ public function init_test_drive_account( string $country, array $capabilities = [] ): bool { if ( ! $this->payments_api_client->is_server_connected() ) { throw new Exception( __( 'Your store is not connected to WordPress.com. Please connect it first.', 'woocommerce-payments' ) ); } if ( $this->is_onboarding_init_in_progress() ) { // We can't allow multiple onboarding initializations to happen at the same time. throw new Exception( __( 'Onboarding initialization is already in progress. Please wait for it to finish.', 'woocommerce-payments' ) ); } // Since there should be no Stripe KYC needed, make sure we start with a clean state. delete_transient( WC_Payments_Account::ONBOARDING_STATE_TRANSIENT ); delete_option( WC_Payments_Account::EMBEDDED_KYC_IN_PROGRESS_OPTION ); $this->set_onboarding_init_in_progress(); $current_user = get_userdata( get_current_user_id() ); // Make sure the onboarding test mode DB flag is set. self::set_test_mode( true ); $site_data = [ 'site_username' => wp_get_current_user()->user_login, 'site_locale' => get_locale(), ]; $user_data = $this->get_onboarding_user_data(); $account_data = $this->get_account_data( 'test_drive', [ 'business_type' => 'individual', 'country' => $country, 'individual' => [ 'first_name' => $current_user->first_name ?? null, 'last_name' => $current_user->last_name ?? null, ], ], $capabilities ); // Attempt to create the account. $onboarding_data = $this->payments_api_client->get_onboarding_data( false, WC_Payments_Account::get_connect_url(), $site_data, WC_Payments_Utils::array_filter_recursive( $user_data ), WC_Payments_Utils::array_filter_recursive( $account_data ), self::get_actioned_notes(), ); // Store the 'woopay_enabled_by_default' flag in a transient, to be enabled later respecting // the WooPay capability value from the request. $should_enable_woopay = $this->should_enable_woopay( filter_var( $onboarding_data['woopay_enabled_by_default'] ?? false, FILTER_VALIDATE_BOOLEAN ), $capabilities ); if ( $should_enable_woopay ) { set_transient( WC_Payments_Account::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT, true, DAY_IN_SECONDS ); } // Our platform will respond with a URL set to false if the account was created and // no further action is needed - which is the case for test-drive accounts. $account_created = isset( $onboarding_data['url'] ) && false === $onboarding_data['url']; if ( $account_created ) { // Set the gateway options. $gateway = WC_Payments::get_gateway(); $gateway->update_option( 'enabled', 'yes' ); $gateway->update_option( 'test_mode', empty( $onboarding_data['is_live'] ) ? 'yes' : 'no' ); // Handle the payment methods settings. if ( ! empty( $capabilities ) ) { $this->update_enabled_payment_methods_ids( $gateway, $capabilities ); } // Store a state after completing KYC for tracks. This is stored temporarily in option because // user might not have agreed to TOS yet. update_option( '_wcpay_onboarding_stripe_connected', [ 'is_existing_stripe_account' => true ] ); } $this->clear_onboarding_init_in_progress(); // Clear the account cache to force a refresh. WC_Payments::get_account_service()->clear_cache(); return $account_created; } /** * Reset the current onboarding state. * * This means: * - delete the currently connected Stripe account - if possible! * - reset the onboarding flags, options, and caches. * * @param array $context Context for the reset onboarding request. * - 'from' (string) The source of the request. * - 'source' (string) The source of the onboarding flow. * * @return bool Whether the onboarding was reset successfully. * * @throws API_Exception When the platform API request fails or is not successful. */ public function reset_onboarding( array $context ): bool { if ( ! $this->payments_api_client->is_server_connected() ) { throw new Exception( __( 'Your store is not connected to WordPress.com. Please connect it first.', 'woocommerce-payments' ) ); } // If the account does not exist, there's nothing to reset. if ( ! WC_Payments::get_account_service()->is_stripe_connected() ) { throw new API_Exception( __( 'Failed to reset the account: account does not exist.', 'woocommerce-payments' ), 'wcpay-onboarding-account-error', 400 ); } // Immediately change the account cache to avoid API requests during the time it takes for // the Transact Platform to actually delete the account. WC_Payments::get_account_service()->overwrite_cache_with_no_account(); // Delete the currently connected Stripe account, in the onboarding mode we are currently in. $test_mode_onboarding = self::is_test_mode_enabled(); $result = $this->payments_api_client->delete_account( $test_mode_onboarding ); if ( ! isset( $result['result'] ) || 'success' !== $result['result'] ) { WC_Payments::get_account_service()->refresh_account_data(); throw new API_Exception( __( 'Failed to delete account.', 'woocommerce-payments' ), 'wcpay-onboarding-account-error', 400 ); } $this->cleanup_on_account_reset(); delete_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ); // Track onboarding reset. $event_properties = [ 'mode' => $test_mode_onboarding ? 'test' : 'live', 'from' => ! empty( $context['from'] ) ? sanitize_text_field( $context['from'] ) : '', 'source' => ! empty( $context['source'] ) ? sanitize_text_field( $context['source'] ) : '', ]; $this->tracks_event( self::TRACKS_EVENT_ONBOARDING_RESET, $event_properties ); return true; } /** * Disable the Test Drive account. * * This means: * - preserve the currently connected Stripe test drive account settings. * - delete the currently connected Stripe test drive account. * - cleanup the gateway state for a fresh onboarding flow. * * @param array $context Context for the disable test drive account request. * - 'from' (string) The source of the request. * - 'source' (string) The source of the onboarding flow. * * @return bool Whether the test drive account was disabled successfully. * * @throws API_Exception When the platform API request fails or is not successful. */ public function disable_test_drive_account( array $context ): bool { if ( ! $this->payments_api_client->is_server_connected() ) { throw new Exception( __( 'Your store is not connected to WordPress.com. Please connect it first.', 'woocommerce-payments' ) ); } // If the account does not exist, there's nothing to disable. if ( ! WC_Payments::get_account_service()->is_stripe_connected() ) { throw new API_Exception( __( 'Failed to activate the account: account does not exist.', 'woocommerce-payments' ), 'wcpay-onboarding-account-error', 400 ); } // If the test mode onboarding is not enabled, we don't need to do anything. if ( ! self::is_test_mode_enabled() ) { return false; } // If the test mode onboarding is enabled: // - Delete the current account; // - Cleanup the gateway state for a fresh onboarding flow. try { // If we're in test mode and dealing with a test-drive account, // we need to collect the test drive settings before we delete the test-drive account, // and apply those settings to the live account. WC_Payments::get_account_service()->save_test_drive_settings(); // Immediately change the account cache to avoid API requests during the time it takes for // the Transact Platform to actually delete the account. WC_Payments::get_account_service()->overwrite_cache_with_no_account(); // Delete the currently connected Stripe account. $this->payments_api_client->delete_account( true ); } catch ( API_Exception $e ) { WC_Payments::get_account_service()->refresh_account_data(); throw new API_Exception( __( 'Failed to disable test drive account.', 'woocommerce-payments' ), 'wcpay-onboarding-account-error', 400 ); } $this->cleanup_on_account_reset(); // Track disabling test drive account. $event_properties = [ 'mode' => self::is_test_mode_enabled() ? 'test' : 'live', 'from' => ! empty( $context['from'] ) ? sanitize_text_field( $context['from'] ) : '', 'source' => ! empty( $context['source'] ) ? sanitize_text_field( $context['source'] ) : '', ]; $this->tracks_event( self::TRACKS_EVENT_TEST_DRIVE_ACCOUNT_DISABLE, $event_properties ); return true; } /** * Sets things up for a fresh onboarding flow. * * @return void */ public function cleanup_on_account_reset() { $gateway = WC_Payments::get_gateway(); $gateway->update_option( 'enabled', 'no' ); $gateway->update_option( 'test_mode', 'no' ); $gateway->update_option( 'upe_enabled_payment_method_ids', [ 'card' ] ); update_option( '_wcpay_onboarding_stripe_connected', [] ); update_option( self::TEST_MODE_OPTION, 'no' ); self::clear_account_options(); // Discard any ongoing onboarding session. delete_transient( WC_Payments_Account::ONBOARDING_STATE_TRANSIENT ); $this->clear_embedded_kyc_in_progress(); delete_transient( WC_Payments_Account::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT ); $this->clear_onboarding_init_in_progress(); // Clear the entire database cache since everything hinges on the account. // If the account is gone, everything else is too. $this->database_cache->delete_all(); } /** * Cleanup onboarding flow data after the account is onboarded. * * This is to avoid keeping unnecessary data in the database. * We focus on data stores in DB options. Transients have a limited lifetime and will be cleaned up automatically. * * @return void */ public function cleanup_on_account_onboarded() { // Delete the onboarding fields data since it is used only during the initial onboarding. $this->database_cache->delete( Database_Cache::ONBOARDING_FIELDS_DATA_KEY ); $this->database_cache->delete( Database_Cache::BUSINESS_TYPES_KEY ); $this->database_cache->delete( Database_Cache::RECOMMENDED_PAYMENT_METHODS ); } /** * Determine whether an embedded KYC flow is in progress. * * @return bool True if embedded KYC is in progress, false otherwise. */ public function is_embedded_kyc_in_progress(): bool { return filter_var( get_option( WC_Payments_Account::EMBEDDED_KYC_IN_PROGRESS_OPTION, 'no' ), FILTER_VALIDATE_BOOLEAN ); } /** * Mark the embedded KYC flow as in progress. * * @return bool Whether we successfully marked the flow as in progress. */ public function set_embedded_kyc_in_progress(): bool { return update_option( WC_Payments_Account::EMBEDDED_KYC_IN_PROGRESS_OPTION, 'yes' ); } /** * Clear any embedded KYC in progress flags. * * @return boolean Whether we successfully cleared the flags. */ public function clear_embedded_kyc_in_progress(): bool { return delete_option( WC_Payments_Account::EMBEDDED_KYC_IN_PROGRESS_OPTION ); } /** * Get actioned notes. * * @return array */ public static function get_actioned_notes(): array { $wcpay_note_names = []; try { /** * Data Store for admin notes * * @var DataStore $data_store */ $data_store = WC_Data_Store::load( 'admin-note' ); } catch ( Exception $e ) { // Don't stop the on-boarding process if something goes wrong here. Log the error and return the empty array // of actioned notes. Logger::error( $e ); return $wcpay_note_names; } // Fetch the last 10 actioned wcpay-promo admin notifications. $add_like_clause = function ( $where_clause ) { return $where_clause . " AND name like 'wcpay-promo-%'"; }; add_filter( 'woocommerce_note_where_clauses', $add_like_clause ); $wcpay_promo_notes = $data_store->get_notes( [ 'status' => [ Note::E_WC_ADMIN_NOTE_ACTIONED ], 'is_deleted' => false, 'per_page' => 10, ] ); remove_filter( 'woocommerce_note_where_clauses', $add_like_clause ); // If we didn't get an array back from the data store, return an empty array of results. if ( ! is_array( $wcpay_promo_notes ) ) { return $wcpay_note_names; } // Copy the name of each note into the results. foreach ( (array) $wcpay_promo_notes as $wcpay_note ) { $note = new Note( $wcpay_note->note_id ); $wcpay_note_names[] = $note->get_name(); } return $wcpay_note_names; } /** * Clear any account options we may want to reset when a new onboarding flow is initialised. * Currently, this deletes two options that store whether the eligibility and connection success modals have been dismissed. * * @return void */ public static function clear_account_options(): void { delete_option( self::ONBOARDING_CONNECTION_SUCCESS_MODAL_OPTION ); } /** * Set onboarding test mode. * * Will also switch the WC_Payments onboarding mode immediately. * * @param boolean $test_mode Whether to enable test mode. * @return void */ public static function set_test_mode( bool $test_mode ): void { update_option( self::TEST_MODE_OPTION, $test_mode ? 'yes' : 'no', true ); // Switch WC_Payments onboarding mode immediately. if ( $test_mode ) { \WC_Payments::mode()->test_mode_onboarding(); } else { \WC_Payments::mode()->live_mode_onboarding(); } } /** * Determine if test mode onboarding is enabled. * * @return bool Whether test mode onboarding is enabled or not. */ public static function is_test_mode_enabled(): bool { // We support the `1` option value also for backward compatibility with version 8.1.0. return in_array( get_option( self::TEST_MODE_OPTION, 'no' ), [ 'yes', '1' ], true ); } /** * Determine what was the immediate previous step that landed us to the current request. * * We take into account the referer and GET params, with the referer having the lowest priority. * The primary intention of the from value is to inform the current step logic and allow it to customize the * behavior and/or UX based on the previous step. * * Note: Consider carefully when carrying over the from value to the next step. * Doing so should mean that we didn't complete any step right now, but just moved the merchant around * (probably through redirects). * * @param string|null $referer Optional. The referer URL. Defaults to wp_get_raw_referer(). * @param array|null $get_params Optional. GET params. Defaults to $_GET. * * @return string The from value or empty string if we could not identify a known value. */ public static function get_from( ?string $referer = null, ?array $get_params = null ): string { $referer = $referer ?? wp_get_raw_referer(); // Ensure we decode the referer URL in case it contains encoded characters in its GET parameters. // This way we don't need to distinguish between `%2F` and `/`. $referer = urldecode( $referer ); $get_params = $get_params ?? $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended /** * ================= * First, we check the `from` GET param. * If the `from` param is already set and not empty, use it. * ================= */ $from_param = isset( $get_params['from'] ) ? sanitize_text_field( wp_unslash( $get_params['from'] ) ) : ''; if ( ! empty( $from_param ) ) { return $from_param; } /** * ================= * Next, we check the action-type GET params as they should only be set when the user takes a certain action. * This means they have higher priority than the other "clues" like `wcpay-connect`, `from`, referer. * ================= */ if ( isset( $get_params['wcpay-disable-onboarding-test-mode'] ) && 'true' === $get_params['wcpay-disable-onboarding-test-mode'] ) { return self::FROM_TEST_TO_LIVE; } if ( isset( $get_params['wcpay-reset-account'] ) && 'true' === $get_params['wcpay-reset-account'] ) { return self::FROM_RESET_ACCOUNT; } /** * ================= * Next, we check the `wcpay-connect` GET param. This should hold valid from values. * If it has a known from value, use it. * ================= */ $wcpay_connect_param = isset( $get_params['wcpay-connect'] ) ? sanitize_text_field( wp_unslash( $get_params['wcpay-connect'] ) ) : ''; if ( in_array( $wcpay_connect_param, [ self::FROM_WCADMIN_PAYMENTS_TASK, self::FROM_WCADMIN_PAYMENTS_SETTINGS, self::FROM_WCADMIN_NOX_IN_CONTEXT, self::FROM_WCADMIN_INCENTIVE, self::FROM_CONNECT_PAGE, self::FROM_OVERVIEW_PAGE, self::FROM_ACCOUNT_DETAILS, self::FROM_ONBOARDING_WIZARD, self::FROM_TEST_TO_LIVE, self::FROM_RESET_ACCOUNT, self::FROM_WPCOM, self::FROM_WPCOM_CONNECTION, self::FROM_STRIPE, ], true ) ) { return $wcpay_connect_param; } /** * ================= * Finally, we check the referer URL as it has the lowest priority. * ================= */ if ( false !== strpos( $referer, 'page=wc-admin&task=payments' ) ) { return self::FROM_WCADMIN_PAYMENTS_TASK; } if ( false !== strpos( $referer, 'page=wc-settings&tab=checkout' ) ) { return self::FROM_WCADMIN_PAYMENTS_SETTINGS; } if ( false !== strpos( $referer, 'page=wc-settings&tab=checkout' ) && false !== strpos( $referer, 'path=/woopayments/onboarding' ) ) { return self::FROM_WCADMIN_NOX_IN_CONTEXT; } if ( false !== strpos( $referer, 'path=/wc-pay-welcome-page' ) ) { return self::FROM_WCADMIN_INCENTIVE; } if ( false !== strpos( $referer, 'path=/payments/connect' ) ) { return self::FROM_CONNECT_PAGE; } if ( false !== strpos( $referer, 'path=/payments/overview' ) ) { return self::FROM_OVERVIEW_PAGE; } if ( false !== strpos( $referer, 'path=/payments/onboarding' ) ) { return self::FROM_ONBOARDING_WIZARD; } if ( false !== strpos( $referer, 'path=/payments/deposits' ) || false !== strpos( $referer, 'path=/payments/payouts' ) ) { return self::FROM_PAYOUTS; } if ( false !== strpos( $referer, 'wordpress.com' ) ) { return self::FROM_WPCOM; } if ( false !== strpos( $referer, 'stripe.com' ) ) { return self::FROM_STRIPE; } // Default to empty string. return ''; } /** * Determine the initial onboarding source from the referer and URL params. * * NOTE: Avoid basing business logic on this since it is primarily intended for tracking purposes. * It is greedy in determining the onboarding source and may not always be accurate. * * @param string|null $referer Optional. The referer URL. Defaults to wp_get_raw_referer(). * @param array|null $get_params Optional. GET params. Defaults to $_GET. * * @return string The source or WC_Payments_Onboarding_Service::SOURCE_UNKNOWN if the source is unknown. */ public static function get_source( ?string $referer = null, ?array $get_params = null ): string { $referer = $referer ?? wp_get_raw_referer(); // Ensure we decode the referer URL in case it contains encoded characters in its GET parameters. // This way we don't need to distinguish between `%2F` and `/`. $referer = urldecode( $referer ); $get_params = $get_params ?? $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $valid_sources = [ self::SOURCE_WCADMIN_PAYMENT_TASK, self::SOURCE_WCADMIN_SETTINGS_PAGE, self::SOURCE_WCADMIN_NOX_IN_CONTEXT, self::SOURCE_WCADMIN_INCENTIVE_PAGE, self::SOURCE_WCPAY_CONNECT_PAGE, self::SOURCE_WCPAY_OVERVIEW_PAGE, self::SOURCE_WCPAY_PAYOUTS_PAGE, self::SOURCE_WCPAY_RESET_ACCOUNT, self::SOURCE_WCPAY_SETUP_LIVE_PAYMENTS, self::SOURCE_WCPAY_FINISH_SETUP_TASK, self::SOURCE_WCPAY_UPDATE_BUSINESS_DETAILS_TASK, self::SOURCE_WCPAY_PO_BANK_ACCOUNT_TASK, self::SOURCE_WCPAY_RECONNECT_WPCOM_TASK, self::SOURCE_WCPAY_GO_LIVE_TASK, self::SOURCE_WCPAY_FINISH_SETUP_TOOL, self::SOURCE_WCPAY_PAYOUT_FAILURE_NOTICE, self::SOURCE_WCPAY_ACCOUNT_DETAILS, ]; /** * ================= * First, we check the `source` GET param. * If the source param is already set and a valid value, use it. * ================= */ $source_param = isset( $get_params['source'] ) ? sanitize_text_field( wp_unslash( $get_params['source'] ) ) : ''; if ( in_array( $source_param, $valid_sources, true ) ) { return $source_param; } /** * ================= * Next, we check the action-type GET params as they should only be set when the user takes a certain action. * This means they have higher priority than the other "clues" like `wcpay-connect`, `from`, referer. * ================= */ if ( isset( $get_params['wcpay-disable-onboarding-test-mode'] ) && 'true' === $get_params['wcpay-disable-onboarding-test-mode'] ) { return self::SOURCE_WCPAY_SETUP_LIVE_PAYMENTS; } if ( isset( $get_params['wcpay-reset-account'] ) && 'true' === $get_params['wcpay-reset-account'] ) { return self::SOURCE_WCPAY_RESET_ACCOUNT; } $wcpay_connect_param = isset( $get_params['wcpay-connect'] ) ? sanitize_text_field( wp_unslash( $get_params['wcpay-connect'] ) ) : ''; $from_param = isset( $get_params['from'] ) ? sanitize_text_field( wp_unslash( $get_params['from'] ) ) : ''; /** * ================= * Next, we check the `wcpay-connect` GET param as it has higher priority than `from` GET param or referer. * ================= */ switch ( $wcpay_connect_param ) { case self::FROM_WCADMIN_PAYMENTS_TASK: return self::SOURCE_WCADMIN_PAYMENT_TASK; case self::FROM_WCADMIN_PAYMENTS_SETTINGS: return self::SOURCE_WCADMIN_SETTINGS_PAGE; case self::FROM_WCADMIN_NOX_IN_CONTEXT: return self::SOURCE_WCADMIN_NOX_IN_CONTEXT; case self::FROM_WCADMIN_INCENTIVE: return self::SOURCE_WCADMIN_INCENTIVE_PAGE; default: break; } /** * ================= * Next, we check the `from` GET param as it has a higher priority than the referer. * * Not all `from` values are taken into account (e.g. we ignore 'WCPAY_ONBOARDING_WIZARD'). * ================= */ switch ( $from_param ) { case self::FROM_WCADMIN_PAYMENTS_TASK: return self::SOURCE_WCADMIN_PAYMENT_TASK; case self::FROM_SETTINGS: case self::FROM_WCADMIN_PAYMENTS_SETTINGS: return self::SOURCE_WCADMIN_SETTINGS_PAGE; case self::FROM_WCADMIN_NOX_IN_CONTEXT: return self::SOURCE_WCADMIN_NOX_IN_CONTEXT; case self::FROM_WCADMIN_INCENTIVE: return self::SOURCE_WCADMIN_INCENTIVE_PAGE; case self::FROM_CONNECT_PAGE: return self::SOURCE_WCPAY_CONNECT_PAGE; case self::FROM_PAYOUTS: return self::SOURCE_WCPAY_PAYOUTS_PAGE; case self::FROM_GO_LIVE_TASK: return self::SOURCE_WCPAY_GO_LIVE_TASK; case self::FROM_ACCOUNT_DETAILS: return self::SOURCE_WCPAY_ACCOUNT_DETAILS; default: break; } $referer_params = []; wp_parse_str( wp_parse_url( $referer, PHP_URL_QUERY ), $referer_params ); /** * ================= * Use the source from the referer URL, if present and valid. * ================= */ $source_param = isset( $referer_params['source'] ) ? sanitize_text_field( wp_unslash( $referer_params['source'] ) ) : ''; if ( ! empty( $source_param ) && in_array( $source_param, $valid_sources, true ) ) { return $source_param; } /** * ================= * Finally, we try to determine the source by what page the request came from. * ================= */ if ( 2 === count( array_intersect_assoc( $referer_params, [ 'page' => 'wc-admin', 'task' => 'payments', ] ) ) ) { return self::SOURCE_WCADMIN_PAYMENT_TASK; } if ( 2 === count( array_intersect_assoc( $referer_params, [ 'page' => 'wc-settings', 'tab' => 'checkout', ] ) ) ) { // Discriminate between the settings page and the NOX in-context onboarding. if ( ! empty( $referer_params['path'] ) && 0 === strpos( $referer_params['path'], '/woopayments/onboarding' ) ) { return self::SOURCE_WCADMIN_NOX_IN_CONTEXT; } return self::SOURCE_WCADMIN_SETTINGS_PAGE; } if ( 2 === count( array_intersect_assoc( $referer_params, [ 'page' => 'wc-admin', 'path' => '/wc-pay-welcome-page', ] ) ) ) { return self::SOURCE_WCADMIN_INCENTIVE_PAGE; } if ( 2 === count( array_intersect_assoc( $referer_params, [ 'page' => 'wc-admin', 'path' => '/payments/connect', ] ) ) ) { return self::SOURCE_WCPAY_CONNECT_PAGE; } if ( 2 === count( array_intersect_assoc( $referer_params, [ 'page' => 'wc-admin', 'path' => '/payments/overview', ] ) ) ) { return self::SOURCE_WCPAY_OVERVIEW_PAGE; } if ( 2 === count( array_intersect_assoc( $referer_params, [ 'page' => 'wc-admin', 'path' => '/payments/deposits', ] ) ) || ( 2 === count( array_intersect_assoc( $referer_params, [ 'page' => 'wc-admin', 'path' => '/payments/payouts', ] ) ) ) ) { return self::SOURCE_WCPAY_PAYOUTS_PAGE; } // Default to an unknown source. return self::SOURCE_UNKNOWN; } /** * If settings are collected from the test-drive account, * include them in the existing arguments when creating the new account. * * @param array $args The request args to create new account. * * @return array The request args, possible updated with the test drive account settings, used to create new account. */ public function maybe_add_test_drive_settings_to_new_account_request( array $args ): array { if ( get_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ) && is_array( get_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ) ) ) { $args['account_data'] = array_merge( $args['account_data'], get_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ) ); delete_transient( WC_Payments_Account::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ); } return $args; } /** * Update payment methods to 'enabled' based on the capabilities * provided during the NOX onboarding process. Merchants can preselect their preferred * payment methods as part of this flow. * * The capabilities are provided in the following format: * * [ * 'card' => true, * 'affirm' => true, * ... * ] * * @param WC_Payment_Gateway_WCPay $gateway Payment gateway instance. * @param array $capabilities Provided capabilities. */ public function update_enabled_payment_methods_ids( $gateway, $capabilities = [] ): void { $enabled_gateways = $gateway->get_upe_enabled_payment_method_ids(); $enabled_payment_methods = array_unique( array_merge( $enabled_gateways, $this->exclude_placeholder_payment_methods( $capabilities ) ) ); // Update the gateway option. $gateway->update_option( 'upe_enabled_payment_method_ids', $enabled_payment_methods ); /** * Keeps the list of enabled payment method IDs synchronized between the default * `woocommerce_woocommerce_payments_settings` and duplicates in individual gateway settings. */ foreach ( $enabled_payment_methods as $payment_method_id ) { $payment_gateway = WC_Payments::get_payment_gateway_by_id( $payment_method_id ); if ( $payment_gateway ) { $payment_gateway->enable(); $payment_gateway->update_option( 'upe_enabled_payment_method_ids', $enabled_payment_methods ); } } // Update gateway option with the WooPay capability. if ( ! empty( $capabilities['woopay'] ) ) { $gateway->update_is_woopay_enabled( true ); } else { $gateway->update_is_woopay_enabled( false ); } // Update Apple/Google Pay gateway enabled state. $google_pay_gateway = WC_Payments::get_payment_gateway_by_id( \WCPay\PaymentMethods\Configs\Definitions\GooglePayDefinition::get_id() ); $apple_pay_gateway = WC_Payments::get_payment_gateway_by_id( \WCPay\PaymentMethods\Configs\Definitions\ApplePayDefinition::get_id() ); if ( ! empty( $capabilities['apple_google'] ) || ( ! empty( $capabilities['apple_pay'] ) || ! empty( $capabilities['google_pay'] ) ) ) { if ( $apple_pay_gateway ) { $apple_pay_gateway->enable(); } if ( $google_pay_gateway ) { $google_pay_gateway->enable(); } } else { if ( $apple_pay_gateway ) { $apple_pay_gateway->disable(); } if ( $google_pay_gateway ) { $google_pay_gateway->disable(); } } } /** * Given a referral code, normalize it and store it in a transient. * * @param string $referral_code The referral code to normalize and store. * * @return string The normalized referral code. */ public function normalize_and_store_referral_code( string $referral_code ): string { $normalized = trim( strtolower( substr( $referral_code, 0, 50 ) ) ); if ( empty( $normalized ) ) { return ''; } set_transient( 'woopayments_referral_code', $normalized, 30 * DAY_IN_SECONDS ); return $normalized; } /** * Get the referral code from the transient. * * @return string|null The referral code or null if not found. */ public function get_referral_code(): ?string { $value = get_transient( 'woopayments_referral_code' ); if ( empty( $value ) ) { return null; } return $value; } /** * Excludes placeholder payment methods and removes duplicates. * * WooPay and Apple Pay & Google Pay are considered placeholder payment methods and are excluded. * * @param array $payment_methods Array of payment methods to process. * * @return array Filtered array of unique payment methods. */ private function exclude_placeholder_payment_methods( array $payment_methods ): array { // Placeholder payment methods. $excluded_methods = [ 'woopay', 'apple_google' ]; return array_filter( array_unique( array_keys( array_filter( $payment_methods ) ) ), function ( $payment_method ) use ( $excluded_methods ) { return ! in_array( $payment_method, $excluded_methods, true ); } ); } /** * Send a Tracks event. * * By default Woo adds `url`, `blog_lang`, `blog_id`, `store_id`, `products_count`, and `wc_version` * properties to every event. * * @todo This is a duplicate of the one in the WC_Payments_Account class. When we refactor the onboarding logic out of the WC_Payments_Account class we should consider a proper place for this method. * * @param string $name The event name. * @param array $properties Optional. The event custom properties. * * @return void */ private function tracks_event( string $name, array $properties = [] ) { if ( ! function_exists( 'wc_admin_record_tracks_event' ) ) { return; } // Add default properties to every event. $account_service = WC_Payments::get_account_service(); $tracking_info = $account_service ? $account_service->get_tracking_info() : []; $properties = array_merge( $properties, [ 'is_test_mode' => WC_Payments::mode()->is_test(), 'jetpack_connected' => $this->payments_api_client->is_server_connected(), 'wcpay_version' => WCPAY_VERSION_NUMBER, 'woo_country_code' => WC()->countries->get_base_country(), ], $tracking_info ?? [] ); wc_admin_record_tracks_event( $name, $properties ); Logger::info( 'Tracks event: ' . $name . ' with data: ' . wp_json_encode( WC_Payments_Utils::redact_array( $properties, [ 'woo_country_code' ] ) ) ); } } includes/subscriptions/class-wc-payments-subscriptions-admin-notices.php 0000777 00000011067 15251156605 0023023 0 ustar 00 <?php /** * Class WC_Payments_Subscriptions_Admin_Notices * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; /** * Class for handling admin notices related to Stripe Billing deprecation. */ class WC_Payments_Subscriptions_Admin_Notices { /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_action( 'admin_notices', [ $this, 'display_stripe_billing_deprecation_notice' ] ); } /** * Display admin notice for Stripe Billing deprecation. */ public function display_stripe_billing_deprecation_notice() { // Only show on subscription-related pages. if ( ! $this->is_subscription_page() ) { return; } // Only show if Stripe Billing is enabled and WooCommerce Subscriptions is not active. if ( ! $this->is_bundled_subscriptions_enabled() ) { return; } $wcpay_version = $this->get_wcpay_version(); // if wcpay version is > 10.2.99, bail to not show the notice indefinitely. if ( version_compare( $wcpay_version, '10.2.99', '>' ) ) { return; } $message = ''; if ( version_compare( $wcpay_version, '9.7.0', '<' ) ) { $message = sprintf( /* translators: %1$s: WooCommerce Subscriptions */ __( '<strong>Important:</strong> From version 9.7 of WooPayments (scheduled for 23 July, 2025), you\'ll <strong>no longer be able to offer new product subscriptions</strong>. To avoid disruption, please install <a target="_blank" href="%1$s">WooCommerce Subscriptions</a>.', 'woocommerce-payments' ), 'https://woocommerce.com/products/woocommerce-subscriptions/' ); } elseif ( version_compare( $wcpay_version, '9.8.0', '<' ) ) { $message = sprintf( /* translators: %1$s: WooCommerce Subscriptions */ __( 'WooPayments no longer allows customers to create new subscriptions. Beginning in version 9.8, billing for existing customer subscriptions will no longer be supported. To ensure there is no interruption of service, please install <a target="_blank" href="%1$s">WooCommerce Subscriptions</a>.', 'woocommerce-payments' ), 'https://woocommerce.com/products/woocommerce-subscriptions/' ); } elseif ( version_compare( $wcpay_version, '9.9.0', '<' ) ) { $message = sprintf( /* translators: %1$s: WooCommerce Subscriptions */ __( 'WooPayments no longer supports billing for existing customer subscriptions. All subscriptions data is read-only. Please install <a target="_blank" href="%1$s">WooCommerce Subscriptions</a> to continue managing your subscriptions.', 'woocommerce-payments' ), 'https://woocommerce.com/products/woocommerce-subscriptions/' ); } else { $message = sprintf( /* translators: %1$s: WooCommerce Subscriptions */ __( 'WooPayments no longer supports subscriptions capabilities and subscriptions data can no longer be accessed. Please install <a target="_blank" href="%1$s">WooCommerce Subscriptions</a> to continue managing your subscriptions.', 'woocommerce-payments' ), 'https://woocommerce.com/products/woocommerce-subscriptions/' ); } WC_Payments::display_admin_notice( $message, 'notice-warning' ); } /** * Check if the current page is a subscription-related page. * * @return bool */ protected function is_subscription_page() { $screen = $this->get_screen_id(); if ( ! $screen ) { return false; } elseif ( false !== strpos( $screen, 'edit-shop_subscription' ) ) { return true; } elseif ( false !== strpos( $screen, 'shop_subscription' ) ) { return true; } elseif ( 'woocommerce_page_wc-settings' === $screen && isset( $_GET['tab'] ) && 'subscriptions' === sanitize_text_field( wp_unslash( $_GET['tab'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return true; } return false; } /** * Get the current screen ID. * * @return string|false */ protected function get_screen_id() { $screen = get_current_screen(); if ( ! $screen ) { return false; } return $screen->id; } /** * Get the WooPayments version. * * @return string */ protected function get_wcpay_version() { return WC_Payments::get_file_version( WCPAY_PLUGIN_FILE ); } /** * Check if bundled subscriptions are enabled. * * This checks for either WCPay Subscriptions or Stripe Billing being enabled, * as both represent the bundled subscription functionality. * * @return bool */ protected function is_bundled_subscriptions_enabled() { $has_bundled_subs = WC_Payments_Features::is_wcpay_subscriptions_enabled() || WC_Payments_Features::is_stripe_billing_enabled(); $has_wc_subs = class_exists( 'WC_Subscriptions' ); return $has_bundled_subs && ! $has_wc_subs; } } includes/subscriptions/class-wc-payments-invoice-service.php 0000777 00000041265 15251156605 0020461 0 ustar 00 <?php /** * Class WC_Payments_Invoice_Service * * @package WooCommerce\Payments */ use WCPay\Core\Server\Request\Get_Intention; use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\Rest_Request_Exception; use WCPay\Exceptions\Order_Not_Found_Exception; use WCPay\Logger; defined( 'ABSPATH' ) || exit; /** * Class handling any subscription invoice functionality. */ class WC_Payments_Invoice_Service { /** * Subscription meta key used to store subscription's last invoice ID. * * @const string */ const PENDING_INVOICE_ID_KEY = '_wcpay_pending_invoice_id'; /** * Meta key used to store invoice IDs on orders. * * @const */ const ORDER_INVOICE_ID_KEY = '_wcpay_billing_invoice_id'; /** * Client for making requests to the WooCommerce Payments API. * * @var WC_Payments_API_Client */ private $payments_api_client; /** * Order Service * * @var WC_Payments_Order_Service */ private $order_service; /** * Constructor. * * @param WC_Payments_API_Client $payments_api_client WooCommerce Payments API client. * @param WC_Payments_Order_Service $order_service WC payments Order Service. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Order_Service $order_service ) { $this->payments_api_client = $payments_api_client; $this->order_service = $order_service; /** * When a store is in staging mode we don't want any order status chagnes to fire off corrisponding invoice requests to the server. * * Sending these requests from staging sites can have unintended consequences for the live store. For example, updating an unpaid * renewal order's status on a duplicate site, would lead to the corrisponding subscription being marked as paid in the live * account at Stripe. */ if ( WC_Payments_Subscriptions::is_duplicate_site() ) { return; } if ( WC_Payments_Features::should_use_stripe_billing() ) { add_action( 'woocommerce_order_payment_status_changed', [ $this, 'maybe_record_invoice_payment' ], 10, 1 ); add_action( 'woocommerce_renewal_order_payment_complete', [ $this, 'maybe_record_invoice_payment' ], 11, 1 ); } } /** * Gets the subscription last invoice ID from WC subscription. * * @param WC_Subscription $subscription The subscription. * * @return string Invoice ID. */ public static function get_pending_invoice_id( $subscription ): string { return $subscription->get_meta( self::PENDING_INVOICE_ID_KEY, true ); } /** * Gets the invoice ID from a WC order. * * @param WC_Order $order The order. * @return string Invoice ID. */ public static function get_order_invoice_id( WC_Order $order ): string { return $order->get_meta( self::ORDER_INVOICE_ID_KEY, true ); } /** * Gets the invoice ID from a WC subscription. * * @param WC_Subscription $subscription The subscription. * * @return string Invoice ID. */ public static function get_subscription_invoice_id( $subscription ) { return $subscription->get_meta( self::ORDER_INVOICE_ID_KEY, true ); } /** * Gets the WC order ID from the invoice ID. * * @param string $invoice_id The invoice ID. * @return int The order ID. */ public static function get_order_id_by_invoice_id( string $invoice_id ) { $query_args = [ 'status' => 'any', 'type' => 'shop_order', 'limit' => 1, 'return' => 'ids', 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query [ 'key' => self::ORDER_INVOICE_ID_KEY, 'value' => $invoice_id, ], ], ]; // On HPOS environments we can pass meta_query directly to get_orders() as WC doesn't override it. if ( WC_Payments_Utils::is_hpos_tables_usage_enabled() ) { $order_ids = wc_get_orders( $query_args ); } else { $meta_query = $query_args['meta_query']; unset( $query_args['meta_query'] ); $add_meta_query = function ( $query ) use ( $meta_query ) { $query['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query return $query; }; add_filter( 'woocommerce_order_data_store_cpt_get_orders_query', $add_meta_query, 10, 2 ); $order_ids = wc_get_orders( $query_args ); remove_filter( 'woocommerce_order_data_store_cpt_get_orders_query', $add_meta_query, 10 ); } return (int) array_shift( $order_ids ); } /** * Sets a pending invoice ID meta for a subscription. * * @param WC_Subscription $subscription The subscription to set the invoice on. * @param string $invoice_id The invoice ID. */ public function mark_pending_invoice_for_subscription( WC_Subscription $subscription, string $invoice_id ) { $this->set_pending_invoice_id( $subscription, $invoice_id ); } /** * Removes pending invoice id meta from subscription. * * @param WC_Subscription $subscription The Subscription. */ public function mark_pending_invoice_paid_for_subscription( WC_Subscription $subscription ) { $this->set_pending_invoice_id( $subscription, '' ); } /** * Marks a subscription invoice as paid after a parent or renewal order is completed. * * When a subscription's parent order goes from a pending payment status to a payment completed status, * or when a subscription with no corresponding Stripe subscription is manually renewed, * make sure the invoice is marked as paid (without charging the customer since it was charged on checkout). * * Note: this function has no impact on staging sites to prevent corrupting the live subscriptions on the WCPay account. * * @param int $order_id The WC order ID. * @throws API_Exception If the request to mark the invoice as paid fails. */ public function maybe_record_invoice_payment( int $order_id ) { if ( ! function_exists( 'wcs_get_subscriptions_for_order' ) ) { return; } $order = wc_get_order( $order_id ); if ( ! $order || self::get_order_invoice_id( $order ) ) { return; } foreach ( wcs_get_subscriptions_for_order( $order, [ 'order_type' => [ 'parent', 'renewal' ] ] ) as $subscription ) { $invoice_id = self::get_subscription_invoice_id( $subscription ); if ( ! $invoice_id || ! WC_Payments_Subscription_Service::is_wcpay_subscription( $subscription ) ) { continue; } try { // Set the invoice status to paid but don't charge the customer by using paid_out_of_band parameter. $this->payments_api_client->charge_invoice( $invoice_id, [ 'paid_out_of_band' => 'true' ] ); } catch ( API_Exception $e ) { // If the invoice was already paid, silently handle that error. Throw all other exceptions. if ( WP_Http::BAD_REQUEST === $e->get_http_code() && false !== stripos( $e->getMessage(), 'invoice is already paid' ) ) { Logger::info( sprintf( 'Invoice for subscription #%s has already been paid.', $subscription->get_id() ) ); } else { throw $e; } } if ( $subscription->is_manual() ) { $subscription->set_requires_manual_renewal( false ); $subscription->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID ); // Copy the payment token used to pay for the order to the subscription. WC_Payments::get_gateway()->update_failing_payment_method( $subscription, $order ); $subscription->save(); } } } /** * Validates a WCPay invoice. * * @param array $wcpay_item_data The WCPay invoice items. * @param array $wcpay_discount_data The WCPay invoice discounts. * @param WC_Subscription $subscription The WC Subscription object. * * @throws API_Exception If updating the WCPay subscription or items fails. * @throws Rest_Request_Exception If WCPay invoice items do not match WC subscription items. */ public function validate_invoice( array $wcpay_item_data, array $wcpay_discount_data, WC_Subscription $subscription ) { $item_data = $this->get_repair_data_for_wcpay_items( $wcpay_item_data, $subscription ); if ( ! empty( $item_data ) ) { foreach ( $item_data as $id => $data ) { $this->payments_api_client->update_subscription_item( $id, $data ); } } $discount_data = $this->get_repair_data_for_wcpay_discounts( $wcpay_discount_data, $subscription ); if ( isset( $discount_data ) ) { $response = $this->payments_api_client->update_subscription( WC_Payments_Subscription_Service::get_wcpay_subscription_id( $subscription ), [ 'discounts' => $discount_data ] ); WC_Payments_Subscription_Service::set_wcpay_discount_ids( $subscription, $response['discounts'] ); } } /** * Sets the subscription's last invoice ID meta for WC subscription. * * @param WC_Order $order The order. * @param string $invoice_id The invoice ID. */ public function set_order_invoice_id( WC_Order $order, string $invoice_id ) { $order->update_meta_data( self::ORDER_INVOICE_ID_KEY, $invoice_id ); $order->save(); } /** * Sets the subscription's last invoice ID meta for WC subscription. * * @param WC_Subscription $subscription The subscription. * @param string $parent_invoice_id The parent order invoice ID. */ public function set_subscription_invoice_id( WC_Subscription $subscription, string $parent_invoice_id ) { $subscription->update_meta_data( self::ORDER_INVOICE_ID_KEY, $parent_invoice_id ); $subscription->save(); } /** * Retrieves the intent object and adds its data to the order. * * @param WC_Order $order The order to update. * @param string $intent_id The intent ID. * * @throws Order_Not_Found_Exception */ public function get_and_attach_intent_info_to_order( $order, $intent_id ) { try { $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); $intent_object = $request->send(); } catch ( API_Exception $e ) { $order->add_order_note( __( 'The payment info couldn\'t be added to the order.', 'woocommerce-payments' ) ); return; } $charge = $intent_object->get_charge(); $this->order_service->attach_intent_info_to_order( $order, $intent_object ); } /** * Sends a request to server to record the store's context for an invoice payment. * * @param string $invoice_id The subscription invoice ID. * * @return array * @throws API_Exception */ public function record_subscription_payment_context( string $invoice_id ) { return $this->payments_api_client->update_invoice( $invoice_id, [ 'subscription_context' => class_exists( 'WC_Subscriptions' ) && WC_Payments_Features::is_stripe_billing_enabled() ? 'stripe_billing' : 'legacy_wcpay_subscription', ] ); } /** * Sends a request to server to update transaction details. * * @param array $invoice Invoice details. * @param WC_Order $order Order details. * * @return void * @throws API_Exception */ public function update_transaction_details( array $invoice, WC_Order $order ) { if ( ! isset( $invoice['charge'] ) ) { return; } $charge = $this->payments_api_client->get_charge( $invoice['charge'] ); if ( ! isset( $charge['balance_transaction'] ) || ! isset( $charge['balance_transaction']['id'] ) ) { return; } $this->payments_api_client->update_transaction( $charge['balance_transaction']['id'], [ 'customer_first_name' => $order->get_billing_first_name(), 'customer_last_name' => $order->get_billing_last_name(), 'customer_email' => $order->get_billing_email(), 'customer_country' => $order->get_billing_country(), ] ); } /** * Update a charge with the order id from invoice. * * @param array $invoice Invoice details. * @param int $order_id Order ID. * * @return void * @throws API_Exception */ public function update_charge_details( array $invoice, int $order_id ) { if ( ! isset( $invoice['charge'] ) ) { return; } $this->payments_api_client->update_charge( $invoice['charge'], [ 'metadata' => [ 'order_id' => $order_id ], ] ); } /** * Get recurring items of passed item (subscription). * * @param WC_Subscription $item Subscription to get recurring items for. * * @return array */ public function get_recurring_items( $item ) { // Subscription service has this service as a dependency, so we can't inject it via constructor. // With this we can mock this function in tests to return whatever we want. return WC_Payments_Subscriptions::get_subscription_service()->get_recurring_item_data_for_subscription( $item ); } /** * Get the WCPay subscription item ID for a WC subscription item. * * @param WC_Order_Item $item The WC subscription item. * * @return string The WCPay subscription item ID. */ public function get_wcpay_item_id( $item ) { return WC_Payments_Subscription_Service::get_wcpay_subscription_item_id( $item ); } /** * Sets the subscription last invoice ID meta for WC subscription. * * @param WC_Subscription $subscription The subscription. * @param string $invoice_id The invoice ID. */ private function set_pending_invoice_id( $subscription, string $invoice_id ) { $subscription->update_meta_data( self::PENDING_INVOICE_ID_KEY, $invoice_id ); $subscription->save(); } /** * Gets repair data for WCPay invoice items. * * @param array $wcpay_item_data The WCPay invoice items. * @param WC_Subscription $subscription The WC Subscription object. * * @return array Repair data. * * @throws Rest_Request_Exception WCPay invoice items do not match WC subscription items. */ private function get_repair_data_for_wcpay_items( array $wcpay_item_data, WC_Subscription $subscription ): array { $repair_data = []; $wcpay_items = []; $subscription_items = $subscription->get_items( [ 'line_item', 'fee', 'shipping', 'tax' ] ); foreach ( $wcpay_item_data as $item ) { $wcpay_subscription_item_id = $item['subscription_item']; $wcpay_items[ $wcpay_subscription_item_id ] = [ 'unit_amount' => $item['price']['unit_amount_decimal'], 'billing_period' => $item['price']['recurring']['interval'], 'billing_interval' => $item['price']['recurring']['interval_count'], 'currency' => $item['price']['currency'], 'quantity' => $item['quantity'], ]; } // Generate any repair data necessary to update the WCPay Subscription so it matches the WC subscription. $recurring_items = $this->get_recurring_items( $subscription ); foreach ( $recurring_items as $recurring_item_data ) { $item_id = $recurring_item_data['metadata']['wc_item_id']; $item = $subscription_items[ $item_id ]; $wcpay_item_id = $this->get_wcpay_item_id( $item ); if ( ! isset( $wcpay_items[ $wcpay_item_id ] ) ) { $message = __( 'The WooPayments invoice items do not match WC subscription items.', 'woocommerce-payments' ); Logger::error( $message ); throw new Rest_Request_Exception( $message ); } // Check the quantity matches between WC and WCPay. if ( $item->is_type( 'line_item' ) && $wcpay_items[ $wcpay_item_id ]['quantity'] !== $recurring_item_data['quantity'] ) { $repair_data[ $wcpay_item_id ]['quantity'] = $recurring_item_data['quantity']; } // Confirm the line item amount matches between WC and WCPay. $unit_amounts_match = (string) $wcpay_items[ $wcpay_item_id ]['unit_amount'] === (string) $recurring_item_data['price_data']['unit_amount_decimal']; if ( ! $unit_amounts_match ) { $price_data = $recurring_item_data['price_data']; // We need to maintain the WCPay subscription's billing terms and currency. ie We cannot update the recurring period, interval or currency mid term. $price_data['currency'] = $wcpay_items[ $wcpay_item_id ]['currency']; $price_data['recurring'] = [ 'interval' => $wcpay_items[ $wcpay_item_id ]['billing_period'], 'interval_count' => $wcpay_items[ $wcpay_item_id ]['billing_interval'], ]; $repair_data[ $wcpay_item_id ]['price_data'] = $price_data; } } return $repair_data; } /** * Gets repair data for WCPay invoice discounts. * * @param array $wcpay_discount_data The WCPay discounts. * @param WC_Subscription $subscription The WC Subscription object. * * @return mixed */ private function get_repair_data_for_wcpay_discounts( array $wcpay_discount_data, WC_Subscription $subscription ) { $repair_data = null; $subscription_discount_ids = WC_Payments_Subscription_Service::get_wcpay_discount_ids( $subscription ); if ( ! empty( $subscription_discount_ids ) || ! empty( $wcpay_discount_data ) ) { if ( count( $subscription_discount_ids ) !== count( $wcpay_discount_data ) ) { $repair_data = WC_Payments_Subscription_Service::get_discount_item_data_for_subscription( $subscription ); } else { foreach ( $subscription_discount_ids as $discount_id ) { if ( ! in_array( $discount_id, $wcpay_discount_data, true ) ) { $repair_data = WC_Payments_Subscription_Service::get_discount_item_data_for_subscription( $subscription ); break; } } } } return $repair_data; } } includes/subscriptions/class-wc-payments-subscriptions-empty-state-manager.php 0000777 00000005052 15251156605 0024152 0 ustar 00 <?php /** * Class WC_Payments_Subscriptions_Empty_State. * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Class for loading WooPayments Subscription empty state screen. */ class WC_Payments_Subscriptions_Empty_State_Manager { use WC_Payments_Subscriptions_Utilities; /** * WC_Payments_Account instance to get information about the account. * * @var WC_Payments_Account */ private $account; /** * WC_Payments_Subscriptions_Empty_State Constructor * * @param WC_Payments_Account $account Account class instance. */ public function __construct( WC_Payments_Account $account ) { $this->account = $account; if ( ! $this->is_subscriptions_plugin_active() ) { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts_and_styles' ] ); } } /** * Enqueues the WCPay Subscription empty state scripts and styles. */ public function enqueue_scripts_and_styles() { $screen = get_current_screen(); // Only enqueue the scripts on the admin subscriptions screen. if ( ! $screen || 'edit-shop_subscription' !== $screen->id ) { return; } if ( ! function_exists( 'wcs_do_subscriptions_exist' ) || wcs_do_subscriptions_exist() ) { return; } WC_Payments::register_script_with_dependencies( 'WCPAY_SUBSCRIPTIONS_EMPTY_STATE', 'dist/subscriptions-empty-state' ); $wcpay_settings = [ 'connectUrl' => WC_Payments_Account::get_connect_url( 'WC_SUBSCRIPTIONS_TABLE' ), 'isConnected' => $this->account->is_stripe_connected(), 'newProductUrl' => WC_Subscriptions_Admin::add_subscription_url(), ]; wp_localize_script( 'WCPAY_SUBSCRIPTIONS_EMPTY_STATE', 'wcpay', $wcpay_settings ); WC_Payments_Utils::enqueue_style( 'WCPAY_SUBSCRIPTIONS_EMPTY_STATE', plugins_url( 'dist/subscriptions-empty-state.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'dist/subscriptions-empty-state.css' ), 'all' ); wp_enqueue_script( 'WCPAY_SUBSCRIPTIONS_EMPTY_STATE' ); } /** * Replaces the default empty subscriptions state HTML with a wrapper for our content to be placed into. * * @deprecated 6.3.0 * @param string $default_empty_state_html The default Subscriptions empty state HTML. * @return string The empty subscriptions sate wrapper. */ public function replace_subscriptions_empty_state( $default_empty_state_html ) { wc_deprecated_function( __FUNCTION__, '6.3.0' ); if ( wcs_do_subscriptions_exist() ) { return $default_empty_state_html; } return '<div id="wcpay_subscriptions_empty_state"></div>'; } } includes/subscriptions/class-wc-payments-subscription-migration-log-handler.php 0000777 00000016204 15251156605 0024267 0 ustar 00 <?php /** * Class WC_Payments_Migration_Log_Handler * * @package WooCommerce\Payments */ use Automattic\Jetpack\Constants; defined( 'ABSPATH' ) || exit; /** * Class for handling Stripe billing -> tokenized migration logs. */ class WC_Payments_Subscription_Migration_Log_Handler { /** * Log handle (name). * * @var string */ const HANDLE = 'woopayments-subscription-migration'; /** * A flag temporarily stored in the context column used to identify DB log entries that have been extended to avoid automated deletion. * * @var array */ const EXTENDED_DB_ENTRY_FLAG = 'extended_migration_log'; /** * The number of years to extend the life of a DB log entry for temporarily. * * @var int */ const DB_ENTRY_EXTENSION_IN_YEARS = 5; /** * The holding property for our WC_Logger instance. * * @var WC_Logger */ private $logger = null; /** * Constructor. */ public function __construct() { // WC Core will delete logs on priority 10, so we need to run before that. if ( $this->has_file_logger_enabled() ) { add_action( 'woocommerce_cleanup_logs', [ $this, 'extend_life_of_migration_file_logs' ], 5 ); } elseif ( $this->has_db_logger_enabled() ) { add_action( 'woocommerce_cleanup_logs', [ $this, 'extend_life_of_migration_db_logs' ], 5 ); add_action( 'woocommerce_cleanup_logs', [ $this, 'restore_db_log_timestamps' ], 100 ); } } /** * Logs a message to the migration log. * * @param string $message The message to log. */ public function log( $message ) { if ( ! $this->logger ) { $this->logger = wc_get_logger(); } $this->logger->debug( $message, [ 'source' => self::HANDLE ] ); } /** * Extends the life of all Stripe billing -> tokenized migration log files to prevent WC Core deleting them. * * WC determines file age by parsing the date from the filename. This function renames files * to include today's date, preventing WC Core from deleting them during log cleanup. */ public function extend_life_of_migration_file_logs() { $log_dir = trailingslashit( WC_LOG_DIR ); foreach ( WC_Log_Handler_File::get_log_files() as $log_file_name ) { // If the log file name starts with our handle, process it. if ( strpos( $log_file_name, self::HANDLE ) === 0 ) { $old_file_path = $log_dir . $log_file_name; $new_file_name = $this->get_updated_log_filename( $log_file_name ); $new_file_path = $log_dir . $new_file_name; // Only rename if the filename would actually change. if ( $new_file_name !== $log_file_name && file_exists( $old_file_path ) ) { // If target file already exists, merge files maintaining chronological order. // Uses stream-based operations for memory efficiency with large log files. if ( file_exists( $new_file_path ) ) { // Append new file content to old file (preserves: old logs first, then new logs). // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen $source_handle = fopen( $new_file_path, 'r' ); if ( false !== $source_handle ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents( $old_file_path, $source_handle, FILE_APPEND ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose fclose( $source_handle ); } // Delete the new file since its content is now merged into old file. // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink unlink( $new_file_path ); } // Rename old file to new filename (with today's date). // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename rename( $old_file_path, $new_file_path ); } } } } /** * Extends the life of all Stripe billing -> tokenized migration DB log entries to prevent WC Core deleting them. * * This function temporarily adds 5 years to all migration log entries, and adds a flag to the context column to * identify them for reinstating the actual timestamp later. Hooked in after this function, * `restore_db_log_timestamps` will reinstate the actual timestamps by subtracting the 5 years. * * @see restore_db_log_timestamps() */ public function extend_life_of_migration_db_logs() { global $wpdb; $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}woocommerce_log SET timestamp = DATE_ADD( timestamp, INTERVAL %d YEAR ), context = %s WHERE source = %s", self::DB_ENTRY_EXTENSION_IN_YEARS, self::EXTENDED_DB_ENTRY_FLAG, self::HANDLE ) ); } /** * Reinstates the actual timestamps of all Stripe billing -> tokenized migration DB log entries. * * This function is hooked in after `extend_life_of_migration_db_logs` to reinstate the actual timestamps * of all migration log entries by subtracting the 5 years that were added to them. * * @see extend_life_of_migration_db_logs() */ public function restore_db_log_timestamps() { global $wpdb; $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}woocommerce_log SET timestamp = DATE_SUB( timestamp, INTERVAL %d YEAR ), context = NULL WHERE source = %s AND context = %s", self::DB_ENTRY_EXTENSION_IN_YEARS, self::HANDLE, self::EXTENDED_DB_ENTRY_FLAG ) ); } /** * Generates an updated log filename with today's date. * * Parses the old filename and replaces the date with today's date while * preserving the hash suffix for uniqueness. * * @param string $old_filename The original log filename. * @return string The updated filename with today's date. */ private function get_updated_log_filename( $old_filename ) { // Expected format: woopayments-subscription-migration-YYYY-MM-DD-hash.log // Parse the filename to extract the hash suffix. $pattern = '/^(' . preg_quote( self::HANDLE, '/' ) . ')-(\d{4}-\d{2}-\d{2})-(.+)\.log$/'; if ( preg_match( $pattern, $old_filename, $matches ) ) { $handle = $matches[1]; // e.g., "woopayments-subscription-migration". $old_date = $matches[2]; // e.g., "2024-02-05". $hash_suffix = $matches[3]; // e.g., "abc123" or "1" (rotation number). $today = gmdate( 'Y-m-d' ); // If the date is already today, no need to change. if ( $old_date === $today ) { return $old_filename; } // Generate new filename with today's date. return "{$handle}-{$today}-{$hash_suffix}.log"; } // If pattern doesn't match (shouldn't happen), return original filename. return $old_filename; } /** * Gets the default log handler class. * * @return string The default log handler class. */ private function get_default_log_handler_class() { $handler_class = Constants::get_constant( 'WC_LOG_HANDLER' ); if ( is_null( $handler_class ) || ! class_exists( $handler_class ) ) { $handler_class = WC_Log_Handler_File::class; } return $handler_class; } /** * Determines whether the file logger is enabled. * * @return boolean */ private function has_file_logger_enabled() { return $this->get_default_log_handler_class() === WC_Log_Handler_File::class; } /** * Determines whether the DB logger is enabled. * * @return boolean */ private function has_db_logger_enabled() { return $this->get_default_log_handler_class() === WC_Log_Handler_DB::class; } } includes/subscriptions/class-wc-payments-product-service.php 0000777 00000101537 15251156605 0020504 0 ustar 00 <?php /** * Class WC_Payments_Product_Service * * @package WooCommerce\Payments */ use WCPay\Exceptions\API_Exception; use WCPay\Logger; defined( 'ABSPATH' ) || exit; /** * Class handling any subscription product functionality */ class WC_Payments_Product_Service { use WC_Payments_Subscriptions_Utilities; /** * The product meta key used to store the product data we last sent to WC Pay as a hash. Used to compare current WC product data with WC Pay data. * * @const string */ const PRODUCT_HASH_KEY = '_wcpay_product_hash'; /** * The live product meta key used to store the product's ID in WC Pay. * * @const string */ const LIVE_PRODUCT_ID_KEY = '_wcpay_product_id_live'; /** * The testmode product meta key used to store the product's ID in WC Pay. * * @const string */ const TEST_PRODUCT_ID_KEY = '_wcpay_product_id_test'; /** * The product price meta key used to store the price data we last sent to WC Pay as a hash. Used to compare current WC product price data with WC Pay data. * * @const string */ const PRICE_HASH_KEY = '_wcpay_product_price_hash'; /** * The product meta key used to store the live product's WC Pay Price object ID. * * @const string */ const LIVE_PRICE_ID_KEY = '_wcpay_product_price_id_live'; /** * The product meta key used to store the testmode product's WC Pay Price object ID. * * @const string */ const TEST_PRICE_ID_KEY = '_wcpay_product_price_id_test'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * Account service. * * @var WC_Payments_Account */ private $account; /** * The list of products we need to update at the end of each request. * * @var array */ private $products_to_update = []; /** * The Stripe account ID. * * @var string */ private $stripe_account_id = null; /** * Constructor. * * @param WC_Payments_API_Client $payments_api_client Payments API client. * @param WC_Payments_Account $account Account service. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account ) { $this->payments_api_client = $payments_api_client; $this->account = $account; /** * When a store is in staging mode, we don't want any product handling to be sent to the server. * * Sending these requests from staging sites can have unintended consequences for the live store. For example, * deleting a subscription product on a staging site would delete the product record at Stripe and that product * would be in use for the live site. */ if ( WC_Payments_Subscriptions::is_duplicate_site() ) { return; } // Only create, update and restore/unarchive WCPay Subscription products when Stripe Billing is active. if ( WC_Payments_Features::should_use_stripe_billing() ) { add_action( 'shutdown', [ $this, 'create_or_update_products' ] ); add_action( 'untrashed_post', [ $this, 'maybe_unarchive_product' ] ); add_action( 'wp_trash_post', [ $this, 'maybe_archive_product' ] ); $this->add_product_update_listeners(); } add_filter( 'woocommerce_duplicate_product_exclude_meta', [ $this, 'exclude_meta_wcpay_product' ] ); } /** * Gets the WC Pay product hash associated with a WC product. * * @param WC_Product $product The product to get the hash for. * @return string The product's hash or an empty string. */ public static function get_wcpay_product_hash( WC_Product $product ): string { return $product->get_meta( self::PRODUCT_HASH_KEY, true ); } /** * Get or create the WC Pay product ID associated with a WC product. * * @param WC_Product $product The product to get the WC Pay ID for. * @param bool|null $test_mode Is WC Pay in test/dev mode. * * @return string The WC Pay product ID or an empty string. * @throws Exception */ public function get_or_create_wcpay_product_id( WC_Product $product, $test_mode = null ): string { // If the subscription product doesn't have a WC Pay product ID, create one. if ( ! $this->has_wcpay_product_id( $product, $test_mode ) ) { $is_current_environment = null === $test_mode || WC_Payments::mode()->is_test() === $test_mode; // Only create a new wcpay product if we're trying to fetch a wcpay product ID in the current environment. if ( $is_current_environment ) { $this->create_product( $product ); } } return $product->get_meta( self::get_wcpay_product_id_meta_key( $test_mode ), true ); } /** * Get the WCPay product ID for an item type. * * @param string $type The item type. * * @return string The WCPay product ID. * @throws API_Exception */ public function get_wcpay_product_id_for_item( string $type ): string { $sanitized_type = self::sanitize_option_key( $type ); $option_key_name = self::get_wcpay_product_id_option( $sanitized_type ); $wcpay_product_id = get_option( $option_key_name ); // Case 1: No product found, create a new one. if ( ! $wcpay_product_id ) { return $this->create_product_for_item_type( $sanitized_type ); } // For existing products, check the linked account. $linked_option_key = self::get_wcpay_product_id_linked_to_key( $sanitized_type ); $linked_account_id = get_option( $linked_option_key ); $stripe_account_id = $this->account->get_stripe_account_id(); // Case 2: Product exists but linked account doesn't, validate and update if needed. if ( ! $linked_account_id ) { try { // Validate that the product exists for the current account. $existing_product = $this->payments_api_client->get_product_by_id( $wcpay_product_id ); if ( $existing_product ) { // Product exists, save with current account ID. $this->save_wcpay_product_data( $wcpay_product_id, $stripe_account_id, $sanitized_type ); return $wcpay_product_id; } else { // Product doesn't exist, create new one. return $this->create_product_for_item_type( $sanitized_type ); } } catch ( \Exception $e ) { // Error occurred, create new product. Logger::log( sprintf( 'Error occurred when fetching product : wcpay_product_id=%s, account_id=%s, error=%s', $wcpay_product_id, $stripe_account_id, $e->getMessage() ) ); return $this->create_product_for_item_type( $sanitized_type ); } } // Case 3: Product exists but for a different Stripe account, create new one. if ( $linked_account_id !== $stripe_account_id ) { return $this->create_product_for_item_type( $sanitized_type ); } // Case 4: Valid product exists for current account. return $wcpay_product_id; } /** * Sanitize option key string to replace space with underscore, and remove special characters. * * @param string $type Non sanitized input. * @return string Sanitized output. */ public static function sanitize_option_key( string $type ) { return sanitize_key( str_replace( ' ', '_', trim( $type ) ) ); } /** * Save wcpay product data across two related options. * * @param string $wcpay_product_id The WooCommerce Payments product ID. * @param string $stripe_account_id The Stripe account identifier. * @param string $type The item type used to construct the option key. * * @return void */ private function save_wcpay_product_data( string $wcpay_product_id, string $stripe_account_id, string $type ) { $sanitized_type = self::sanitize_option_key( $type ); $option_key_name = self::get_wcpay_product_id_option( $sanitized_type ); // Store product ID. update_option( $option_key_name, $wcpay_product_id ); // Store linked stripe account ID. $linked_option_key = self::get_wcpay_product_id_linked_to_key( $sanitized_type ); update_option( $linked_option_key, $stripe_account_id ); } /** * Check if the WC product has a valid WC Pay product ID linked to the current Stripe account. * * @param WC_Product $product The product to get the WC Pay ID for. * @param bool|null $test_mode Is WC Pay in test/dev mode. * * @return bool Whether the product has a valid WCPay product ID. */ public function has_wcpay_product_id( WC_Product $product, $test_mode = null ): bool { $option_key = self::get_wcpay_product_id_meta_key( $test_mode ); $wcpay_product_id = $product->get_meta( $option_key ); // No product ID exists. if ( empty( $wcpay_product_id ) ) { return false; } // Check if we have the linked account metadata. $linked_option_key = self::get_wcpay_product_id_linked_to_key( null, $test_mode ); $linked_account_id = $product->get_meta( $linked_option_key ); $current_account_id = $this->account->get_stripe_account_id(); // If we have linked account metadata, just compare with current account. if ( ! empty( $linked_account_id ) ) { return $linked_account_id === $current_account_id; } // Legacy case: we have a product ID but no linked account. // Verify if product exists for current account. try { $product_data = $this->payments_api_client->get_product_by_id( $wcpay_product_id ); // Product exists, update metadata with current account. if ( ! empty( $product_data ) ) { $product->update_meta_data( $linked_option_key, $current_account_id ); $product->save(); return true; } return false; } catch ( \Exception $e ) { Logger::log( sprintf( 'Error validating WooPayments product: product_id=%d, wcpay_product_id=%s, account_id=%s, error=%s', $product->get_id(), $wcpay_product_id, $current_account_id, $e->getMessage() ) ); return false; } } /** * Prevents duplicate WC Pay product IDs and hashes when duplicating a subscription product. * * @param array $meta_keys The keys to exclude from the duplicate. * @return array Keys to exclude. */ public static function exclude_meta_wcpay_product( $meta_keys ) { return array_merge( $meta_keys, [ self::PRODUCT_HASH_KEY, self::LIVE_PRODUCT_ID_KEY, self::TEST_PRODUCT_ID_KEY, self::PRICE_HASH_KEY, self::LIVE_PRICE_ID_KEY, self::TEST_PRICE_ID_KEY, ] ); } /** * Schedules a subscription product to be created or updated in WC Pay on shutdown. * * @since 3.2.0 * * @param int $product_id The ID of the product to handle. */ public function maybe_schedule_product_create_or_update( int $product_id ) { if ( ! class_exists( 'WC_Subscriptions_Product' ) ) { return; } // Skip products which have already been scheduled or aren't subscriptions. $product = wc_get_product( $product_id ); if ( ! $product || isset( $this->products_to_update[ $product_id ] ) || ! WC_Subscriptions_Product::is_subscription( $product ) ) { return; } foreach ( $this->get_products_to_update( $product ) as $product_to_update ) { // Skip products already scheduled. if ( isset( $this->products_to_update[ $product_to_update->get_id() ] ) ) { continue; } // Skip product variations that don't have a price set. if ( $product_to_update->is_type( 'subscription_variation' ) && '' === $product_to_update->get_price() ) { continue; } if ( ! $this->has_wcpay_product_id( $product_to_update ) || $this->product_needs_update( $product_to_update ) ) { $this->products_to_update[ $product_to_update->get_id() ] = $product_to_update->get_id(); } } } /** * Creates and updates all products which have been scheduled for an update. * * Hooked onto shutdown so all products which have been changed in the current request can be updated once. * * @since 3.2.0 */ public function create_or_update_products() { foreach ( $this->products_to_update as $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product ) { continue; } $this->update_products( $product ); } } /** * Creates a product in WC Pay. * * @param WC_Product $product The product to create. */ public function create_product( WC_Product $product ) { try { $product_data = $this->get_product_data( $product ); $stripe_account_id = $this->account->get_stripe_account_id(); // Validate that we have enough data to create the product. $this->validate_product_data( $product_data ); $wcpay_product = $this->payments_api_client->create_product( $product_data ); $this->remove_product_update_listeners(); $this->set_wcpay_product_hash( $product, $this->get_product_hash( $product ) ); $this->set_wcpay_product_id( $product, $wcpay_product['wcpay_product_id'], $stripe_account_id ); $this->add_product_update_listeners(); } catch ( \Exception $e ) { Logger::log( sprintf( 'There was a problem creating the product #%s in WC Pay: %s', $product->get_id(), $e->getMessage() ) ); } } /** * Create a generic item product in WC Pay. * * @param string $type The item type to create a product for. * * @return string The created WCPay product ID. * @throws API_Exception */ private function create_product_for_item_type( string $type ): string { $wcpay_product = $this->payments_api_client->create_product( [ 'description' => 'N/A', 'name' => ucfirst( $type ), ] ); $stripe_account_id = $this->account->get_stripe_account_id(); $this->save_wcpay_product_data( $wcpay_product['wcpay_product_id'], $stripe_account_id, $type ); return $wcpay_product['wcpay_product_id']; } /** * Updates related products in WC Pay when a WC Product is updated. * * @param WC_Product $product The product to update. */ public function update_products( WC_Product $product ) { if ( ! class_exists( 'WC_Subscriptions_Product' ) || ! WC_Subscriptions_Product::is_subscription( $product ) ) { return; } $wcpay_product_ids = $this->get_all_wcpay_product_ids( $product ); $test_mode = WC_Payments::mode()->is_test(); // If the current environment doesn't have a product ID, make sure we create one. if ( ! isset( $wcpay_product_ids[ $test_mode ? 'test' : 'live' ] ) ) { $this->create_product( $product ); } // Return when there's no products to update. if ( empty( $wcpay_product_ids ) ) { return; } if ( ! $this->product_needs_update( $product ) ) { return; } $data = $this->get_product_data( $product ); $this->remove_product_update_listeners(); try { // Validate that we have enough data to create the product. $this->validate_product_data( $data ); // Update all versions of WCPay Products that need updating. foreach ( $wcpay_product_ids as $environment => $wcpay_product_id ) { $data['test_mode'] = 'live' !== $environment; $this->payments_api_client->update_product( $wcpay_product_id, $data ); } $this->set_wcpay_product_hash( $product, $this->get_product_hash( $product ) ); } catch ( \Exception $e ) { Logger::log( sprintf( 'There was a problem updating the product #%s in WC Pay: %s', $product->get_id(), $e->getMessage() ) ); } $this->add_product_update_listeners(); } /** * Archives a subscription product in WC Pay. * * @since 3.2.0 * * @param int $post_id The ID of the post to handle. Only subscription product IDs will be archived in WC Pay. */ public function maybe_archive_product( int $post_id ) { if ( ! class_exists( 'WC_Subscriptions_Product' ) ) { return; } $product = wc_get_product( $post_id ); if ( $product && WC_Subscriptions_Product::is_subscription( $product ) ) { foreach ( $this->get_products_to_update( $product ) as $product ) { $this->archive_product( $product ); } } } /** * Unarchives a subscription product in WC Pay. * * @since 3.2.0 * * @param int $post_id The ID of the post to handle. Only Subscription product post IDs will be unarchived in WC Pay. */ public function maybe_unarchive_product( int $post_id ) { if ( ! class_exists( 'WC_Subscriptions_Product' ) ) { return; } $product = wc_get_product( $post_id ); if ( $product && WC_Subscriptions_Product::is_subscription( $product ) ) { foreach ( $this->get_products_to_update( $product ) as $product ) { $this->unarchive_product( $product ); } } } /** * Archives all related WCPay products (live and test) when a product is trashed/deleted in WC. * * @param WC_Product $product The product to archive. */ public function archive_product( WC_Product $product ) { $wcpay_product_ids = $this->get_all_wcpay_product_ids( $product ); if ( empty( $wcpay_product_ids ) ) { return; } foreach ( $wcpay_product_ids as $environment => $wcpay_product_id ) { try { $this->delete_all_wcpay_price_ids( $product ); $this->payments_api_client->update_product( $wcpay_product_id, [ 'active' => 'false', 'test_mode' => 'live' !== $environment, ] ); } catch ( API_Exception $e ) { Logger::log( 'There was a problem archiving the ' . $environment . ' product in WC Pay: ' . $e->getMessage() ); } } } /** * Unarchives all related WCPay products (live and test) when a product in WC is untrashed. * * @param WC_Product $product The product unarchive. */ public function unarchive_product( WC_Product $product ) { $wcpay_product_ids = $this->get_all_wcpay_product_ids( $product ); if ( empty( $wcpay_product_ids ) ) { return; } foreach ( $wcpay_product_ids as $environment => $wcpay_product_id ) { try { $this->payments_api_client->update_product( $wcpay_product_id, [ 'active' => 'true', 'test_mode' => 'live' !== $environment, ] ); } catch ( API_Exception $e ) { Logger::log( 'There was a problem unarchiving the ' . $environment . 'product in WC Pay: ' . $e->getMessage() ); } } } /** * Archives a WC Pay price object. * * @param string $wcpay_price_id The price object's ID to archive. * @param bool|null $test_mode Is WC Pay in test/dev mode. */ public function archive_price( string $wcpay_price_id, $test_mode = null ) { $data = [ 'active' => 'false' ]; if ( null !== $test_mode ) { $data['test_mode'] = $test_mode; } $this->payments_api_client->update_price( $wcpay_price_id, $data ); } /** * Prevents the subscription interval to be greater than 1 for yearly subscriptions. * * @param int $product_id ID of the product that's being saved. */ public function limit_subscription_product_intervals( $product_id ) { if ( $this->is_subscriptions_plugin_active() || ! class_exists( 'WC_Subscriptions_Product' ) ) { return; } // Skip products that aren't subscriptions. $product = wc_get_product( $product_id ); if ( ! $product || ! WC_Subscriptions_Product::is_subscription( $product ) || empty( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wcsnonce'] ), 'wcs_subscription_meta' ) ) { return; } // If we don't have both the period and the interval, there's nothing to do here. if ( empty( $_REQUEST['_subscription_period'] ) || empty( $_REQUEST['_subscription_period_interval'] ) ) { return; } $period = sanitize_text_field( wp_unslash( $_REQUEST['_subscription_period'] ) ); $interval = absint( wp_unslash( $_REQUEST['_subscription_period_interval'] ) ); // Prevent WC Subs Core from saving the interval when it's invalid. if ( ! $this->is_valid_billing_cycle( $period, $interval ) ) { $new_interval = $this->get_period_interval_limit( $period ); $_REQUEST['_subscription_period_interval'] = (string) $new_interval; /* translators: %1$s Opening strong tag, %2$s Closing strong tag, %3$s The subscription renewal interval (every x time) */ wcs_add_admin_notice( sprintf( __( '%1$sThere was an issue saving your product!%2$s A subscription product\'s billing period cannot be longer than one year. We have updated this product to renew every %3$s.', 'woocommerce-payments' ), '<strong>', '</strong>', wcs_get_subscription_period_strings( $new_interval, $period ) ), 'error' ); } } /** * Prevents the subscription interval to be greater than 1 for yearly subscription variations. * * @param int $product_id Post ID of the variation. * @param int $index Variation index in the incoming array. */ public function limit_subscription_variation_intervals( $product_id, $index ) { if ( $this->is_subscriptions_plugin_active() || ! class_exists( 'WC_Subscriptions_Product' ) ) { return; } // Skip products that aren't subscriptions. $product = wc_get_product( $product_id ); $admin_notice_sent = false; if ( ! $product || ! WC_Subscriptions_Product::is_subscription( $product ) || empty( $_POST['_wcsnonce_save_variations'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wcsnonce_save_variations'] ), 'wcs_subscription_variations' ) ) { return; } // If we don't have both the period and the interval, there's nothing to do here. if ( empty( $_POST['variable_subscription_period'][ $index ] ) || empty( $_POST['variable_subscription_period_interval'][ $index ] ) ) { return; } $period = sanitize_text_field( wp_unslash( $_POST['variable_subscription_period'][ $index ] ) ); $interval = absint( wp_unslash( $_POST['variable_subscription_period_interval'][ $index ] ) ); // Prevent WC Subs Core from saving the interval when it's invalid. if ( ! $this->is_valid_billing_cycle( $period, $interval ) ) { $new_interval = $this->get_period_interval_limit( $period ); $_POST['variable_subscription_period_interval'][ $index ] = (string) $new_interval; if ( false === $admin_notice_sent ) { $admin_notice_sent = true; /* translators: %1$s Opening strong tag, %2$s Closing strong tag */ wcs_add_admin_notice( sprintf( __( '%1$sThere was an issue saving your variations!%2$s A subscription product\'s billing period cannot be longer than one year. We have updated one or more of this product\'s variations to renew every %3$s.', 'woocommerce-payments' ), '<strong>', '</strong>', wcs_get_subscription_period_strings( $new_interval, $period ) ), 'error' ); } } } /** * Attaches the callbacks used to update product changes in WC Pay. */ private function add_product_update_listeners() { // This needs to run before WC_Subscriptions_Admin::save_subscription_meta(), which has a priority of 11. add_action( 'save_post', [ $this, 'limit_subscription_product_intervals' ], 10 ); // This needs to run before WC_Subscriptions_Admin::save_product_variation(), which has a priority of 20. add_action( 'woocommerce_save_product_variation', [ $this, 'limit_subscription_variation_intervals' ], 19, 2 ); add_action( 'save_post_product', [ $this, 'maybe_schedule_product_create_or_update' ], 12 ); add_action( 'woocommerce_save_product_variation', [ $this, 'maybe_schedule_product_create_or_update' ], 30 ); } /** * Removes the callbacks used to update product changes in WC Pay. */ private function remove_product_update_listeners() { remove_action( 'save_post', [ $this, 'limit_subscription_product_intervals' ], 10 ); remove_action( 'woocommerce_save_product_variation', [ $this, 'limit_subscription_variation_intervals' ], 19 ); remove_action( 'save_post_product', [ $this, 'maybe_schedule_product_create_or_update' ], 12 ); remove_action( 'woocommerce_save_product_variation', [ $this, 'maybe_schedule_product_create_or_update' ], 30 ); } /** * Gets product data relevant to WC Pay from a WC product. * * @param WC_Product $product The product to get data from. * @return array */ private function get_product_data( WC_Product $product ): array { return [ 'description' => $product->get_description() ? $product->get_description() : 'N/A', 'name' => $product->get_name(), ]; } /** * Gets the products to update from a given product. * * If applicable, returns the product's variations otherwise returns the product by itself. * * @param WC_Product|WC_Product_Variable $product The product. * * @return array The products to update. */ private function get_products_to_update( WC_Product $product ): array { return $product->is_type( 'variable-subscription' ) ? $product->get_available_variations( 'object' ) : [ $product ]; } /** * Gets a hash of the product's name and description. * Used to compare WC changes with WC Pay data. * * @param WC_Product $product The product to generate the hash for. * @return string The product's hash. */ private function get_product_hash( WC_Product $product ): string { return md5( implode( $this->get_product_data( $product ) ) ); } /** * Checks if a product needs to be updated in WC Pay. * * @param WC_Product $product The product to check updates for. * * @return bool Whether the product needs to be update in WC Pay. */ private function product_needs_update( WC_Product $product ): bool { return $this->get_product_hash( $product ) !== static::get_wcpay_product_hash( $product ); } /** * Sets a WC Pay product hash on a WC product. * * @param WC_Product $product The product to set the WC Pay product hash for. * @param string $value The WC Pay product hash. */ private function set_wcpay_product_hash( WC_Product $product, string $value ) { $product->update_meta_data( self::PRODUCT_HASH_KEY, $value ); $product->save(); } /** * Sets a WC Pay product ID and the Stripe account it's linked to on a WC product. * * @param WC_Product $product The product to set the WC Pay ID for. * @param string $wcpay_product_id The WC Pay product ID. * @param string $stripe_account_id The Stripe account ID. */ private function set_wcpay_product_id( WC_Product $product, string $wcpay_product_id, string $stripe_account_id ) { $option_key = self::get_wcpay_product_id_meta_key(); $link_key = self::get_wcpay_product_id_linked_to_key(); $product->update_meta_data( $option_key, $wcpay_product_id ); $product->update_meta_data( $link_key, $stripe_account_id ); $product->save(); } /** * Returns the name of the product id option meta, taking test mode into account. * * @param string|null $type The item type. * @param bool|null $test_mode Is WC Pay in test/dev mode. * * @return string The WCPay product ID meta key/option name. */ public static function get_wcpay_product_id_option( ?string $type = null, ?bool $test_mode = null ): string { $test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode; $key = $test_mode ? self::TEST_PRODUCT_ID_KEY : self::LIVE_PRODUCT_ID_KEY; return $type ? $key . '_' . $type : $key; } /** * Returns the name of the product id linked to account option meta, taking test mode into account. * * @param string|null $type The item type. * @param bool|null $test_mode Is WC Pay in test/dev mode. * * @return string The WCPay product ID meta key/option name. */ public static function get_wcpay_product_id_linked_to_key( ?string $type = null, ?bool $test_mode = null ): string { $test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode; $key = $test_mode ? self::TEST_PRODUCT_ID_KEY : self::LIVE_PRODUCT_ID_KEY; return ( $type ? $key . '_' . $type : $key ) . '_linked_to'; } /** * Returns the name of the wcpay product id meta key. * * @param bool|null $test_mode Is WCPay in test, prod or dev mode. * * @return string The product id meta key. * @throws Exception */ public static function get_wcpay_product_id_meta_key( $test_mode = null ): string { // This functions looks the same as the one above. // It's here to avoid potential issue when we change the above function. $test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode; return $test_mode ? self::TEST_PRODUCT_ID_KEY : self::LIVE_PRODUCT_ID_KEY; } /** * Returns the name of the price id option meta, taking test mode into account. * * @param bool|null $test_mode Is WC Pay in test/dev mode. * * @return string The price hash option name. */ public static function get_wcpay_price_id_option( $test_mode = null ): string { $test_mode = null === $test_mode ? WC_Payments::mode()->is_test() : $test_mode; return $test_mode ? self::TEST_PRICE_ID_KEY : self::LIVE_PRICE_ID_KEY; } /** * Gets all WCPay Product IDs linked to a WC Product (live and testmode products). * * @param WC_Product $product The product to fetch WCPay product IDs for. * * @return array Live and test WCPay Product IDs if they exist. */ private function get_all_wcpay_product_ids( WC_Product $product ) { $environment_product_ids = [ 'live' => $this->has_wcpay_product_id( $product, false ) ? $this->get_or_create_wcpay_product_id( $product, false ) : null, 'test' => $this->has_wcpay_product_id( $product, true ) ? $this->get_or_create_wcpay_product_id( $product, true ) : null, ]; return array_filter( $environment_product_ids ); } /** * Returns whether the billing cycle is valid, given its period and interval. * * @param string $period Cycle period. * @param int $interval Cycle interval. * @return boolean */ public function is_valid_billing_cycle( $period, $interval ) { $interval_limit = $this->get_period_interval_limit( $period ); // A cycle is valid when we have a defined limit, and the given interval isn't 0 nor greater than the limit. return $interval_limit && ! empty( $interval ) && $interval <= $interval_limit; } /** * Returns the interval limit for the given period. * * @param string $period The period to get the interval limit for. * @return int|bool The interval limit for the period, or false if not defined. */ private function get_period_interval_limit( $period ) { $max_intervals = [ 'year' => 1, 'month' => 12, 'week' => 52, 'day' => 365, ]; return ! empty( $max_intervals[ $period ] ) ? $max_intervals[ $period ] : false; } /** * Deletes and archives a product WCPay Price IDs. * * @param WC_Product $product The WC Product object to delete and archive the a price IDs. */ private function delete_all_wcpay_price_ids( $product ) { // Delete and archive all price IDs for all environments. foreach ( [ 'test', 'live' ] as $environment ) { $test_mode = 'test' === $environment; $price_id_meta_key = self::get_wcpay_price_id_option( $test_mode ); if ( $product->meta_exists( $price_id_meta_key ) ) { try { $this->archive_price( $product->get_meta( $price_id_meta_key, true ), $test_mode ); } catch ( API_Exception $e ) { Logger::log( 'There was a problem archiving the ' . $environment . 'product price ID in WC Pay: ' . $e->getMessage() ); } // Now that the price has been archived, delete the record of it. $product->delete_meta_data( $price_id_meta_key ); $product->delete_meta_data( $price_id_meta_key . '_linked_to' ); } } $product->delete_meta_data( self::PRICE_HASH_KEY ); $product->save(); } /** * Validates that we have the data necessary to create a product in WCPay. * * @param array $product_data Data used to create/update the product in WCPay. * @throws Exception If the product data doesn't contain the 'name' argument as the 'name' property is a required field. */ private function validate_product_data( $product_data ) { if ( empty( $product_data['name'] ) ) { throw new Exception( 'The product "name" is required.' ); } } /** * Deprecated functions */ /** * Unarchives a WC Pay Price object. * * @deprecated 3.3.0 * * @param string $wcpay_price_id The Price object's ID to unarchive. * @param bool|null $test_mode Is WC Pay in test/dev mode. */ public function unarchive_price( string $wcpay_price_id, $test_mode = null ) { wc_deprecated_function( __FUNCTION__, '3.3.0' ); $data = [ 'active' => 'true' ]; if ( null !== $test_mode ) { $data['test_mode'] = $test_mode; } $this->payments_api_client->update_price( $wcpay_price_id, $data ); } /** * Gets the WC Pay price hash associated with a WC product. * * @deprecated 3.3.0 * * @param WC_Product $product The product to get the hash for. * @return string The product's price hash or an empty string. */ public static function get_wcpay_price_hash( WC_Product $product ): string { wc_deprecated_function( __FUNCTION__, '3.3.0' ); return $product->get_meta( self::PRICE_HASH_KEY, true ); } /** * Gets the WC Pay price ID associated with a WC product. * * @deprecated 3.3.0 * * @param WC_Product $product The product to get the WC Pay price ID for. * @param bool|null $test_mode Is WC Pay in test/dev mode. * * @return string The product's WC Pay price ID or an empty string. */ public function get_wcpay_price_id( WC_Product $product, $test_mode = null ): string { wc_deprecated_function( __FUNCTION__, '3.3.0' ); $price_id = $product->get_meta( self::get_wcpay_price_id_option( $test_mode ), true ); if ( ! class_exists( 'WC_Subscriptions_Product' ) ) { return $price_id; } // If the subscription product doesn't have a WC Pay price ID, create one now. if ( empty( $price_id ) && WC_Subscriptions_Product::is_subscription( $product ) ) { $is_current_environment = null === $test_mode || WC_Payments::mode()->is_test() === $test_mode; // Only create WCPay Price object if we're trying to getch a wcpay price ID in the current environment. if ( $is_current_environment ) { $this->create_product( $product ); $price_id = $product->get_meta( self::get_wcpay_price_id_option(), true ); } } return $price_id; } } includes/subscriptions/class-wc-payments-subscription-service.php 0000777 00000125232 15251156605 0021546 0 ustar 00 <?php /** * Class WC_Payments_Subscription_Service * * @package WooCommerce\Payments */ use WCPay\Constants\Order_Mode; use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\Amount_Too_Small_Exception; use WCPay\Exceptions\Cannot_Combine_Currencies_Exception; use WCPay\Exceptions\Subscription_Mode_Mismatch_Exception; use WCPay\Logger; /** * Subscriptions logic for WCPay Subscriptions */ class WC_Payments_Subscription_Service { use WC_Payments_Subscriptions_Utilities; /** * WCPay subscriptions endpoint on server. * * @const string */ const SUBSCRIPTION_API_PATH = '/subscriptions'; /** * Subscription meta key used to store WCPay subscription's ID. * * @const string */ const SUBSCRIPTION_ID_META_KEY = '_wcpay_subscription_id'; /** * Subscription item meta key used to store WCPay subscription item's ID. * * @const string */ const SUBSCRIPTION_ITEM_ID_META_KEY = '_wcpay_subscription_item_id'; /** * Subscription discounts meta key used to store WCPay subscription discount IDs. * * @const string */ const SUBSCRIPTION_DISCOUNT_IDS_META_KEY = '_wcpay_subscription_discount_ids'; /** * WC Payments API Client * * @var WC_Payments_API_Client */ private $payments_api_client; /** * Customer Service * * @var WC_Payments_Customer_Service */ private $customer_service; /** * Product Service * * @var WC_Payments_Product_Service */ private $product_service; /** * Invoice Service * * @var WC_Payments_Invoice_Service */ private $invoice_service; /** * The features WCPay Subscriptions Support. * * @var array */ private $supports = [ 'gateway_scheduled_payments', 'multiple_subscriptions', 'subscription_cancellation', 'subscription_payment_method_change_admin', 'subscription_payment_method_change_customer', 'subscription_payment_method_change', 'subscription_reactivation', 'subscription_suspension', 'subscriptions', ]; /** * A set of temporary exceptions to the limited feature support. * * @var array */ private $feature_support_exceptions = []; /** * Whether the current request is creating a WCPay subscription when * updating the subscription payment method from the "My account" page. * * @var bool */ private $is_creating_subscription_from_update_payment_method = false; /** * WC Payments Subscriptions Constructor. * * Attaches callbacks for managing WC Subscriptions. * * @param WC_Payments_API_Client $api_client WC payments API Client. * @param WC_Payments_Customer_Service $customer_service WC payments customer service. * @param WC_Payments_Product_Service $product_service WC payments Products service. * @param WC_Payments_Invoice_Service $invoice_service WC payments Invoice service. */ public function __construct( WC_Payments_API_Client $api_client, WC_Payments_Customer_Service $customer_service, WC_Payments_Product_Service $product_service, WC_Payments_Invoice_Service $invoice_service ) { $this->payments_api_client = $api_client; $this->customer_service = $customer_service; $this->product_service = $product_service; $this->invoice_service = $invoice_service; /** * When a store is in staging mode, we don't want any subscription updates or purchases to be sent to the server. * * Sending these requests from staging sites can have unintended consequences for the live store. For example, * Subscriptions which renew on the staging site will lead to pausing the shared subscription record at Stripe * and that will result in inexplicable paused subscriptions and missed renewal payments for the live site. */ if ( WC_Payments_Subscriptions::is_duplicate_site() ) { return; } if ( WC_Payments_Features::should_use_stripe_billing() ) { add_action( 'woocommerce_checkout_subscription_created', [ $this, 'create_subscription' ] ); add_action( 'woocommerce_renewal_order_payment_complete', [ $this, 'create_subscription_for_manual_renewal' ] ); add_action( 'woocommerce_subscription_payment_method_updated', [ $this, 'maybe_create_subscription_from_update_payment_method' ], 10, 2 ); } if ( class_exists( 'WC_Subscription' ) ) { // Save the new token on the WCPay subscription when it's added to a WC subscription. add_action( 'woocommerce_payment_token_added_to_order', [ $this, 'update_wcpay_subscription_payment_method' ], 10, 3 ); add_action( 'woocommerce_subscription_status_cancelled', [ $this, 'cancel_subscription' ] ); add_action( 'woocommerce_subscription_status_expired', [ $this, 'cancel_subscription' ] ); add_action( 'woocommerce_subscription_status_on-hold', [ $this, 'handle_subscription_status_on_hold' ] ); add_action( 'woocommerce_subscription_status_pending-cancel', [ $this, 'set_pending_cancel_for_subscription' ] ); add_action( 'woocommerce_subscription_status_pending-cancel_to_active', [ $this, 'reactivate_subscription' ] ); add_action( 'woocommerce_subscription_status_on-hold_to_active', [ $this, 'reactivate_subscription' ] ); add_filter( 'woocommerce_subscription_payment_gateway_supports', [ $this, 'prevent_wcpay_subscription_changes' ], 10, 3 ); add_filter( 'woocommerce_order_actions', [ $this, 'prevent_wcpay_manual_renewal' ], 11, 1 ); add_action( 'woocommerce_payments_changed_subscription_payment_method', [ $this, 'maybe_attempt_payment_for_subscription' ], 10, 2 ); add_action( 'woocommerce_admin_order_data_after_billing_address', [ $this, 'show_wcpay_subscription_id' ] ); add_action( 'woocommerce_subscription_payment_method_updated_from_' . WC_Payment_Gateway_WCPay::GATEWAY_ID, [ $this, 'maybe_cancel_subscription' ], 10, 2 ); add_action( 'wcs_renewal_order_items', [ $this, 'check_wcpay_mode_for_subscription' ], 10, 3 ); } } /** * Checks if the WC subscription has a first payment date that is in the future. * * @param WC_Subscription $subscription WC subscription to check if first payment is now or delayed. * * @return bool Whether the first payment is delayed. */ public static function has_delayed_payment( WC_Subscription $subscription ) { $trial_end = $subscription->get_time( 'trial_end' ); $has_sync = false; if ( ! class_exists( 'WC_Subscriptions_Synchroniser' ) ) { return $has_sync; } if ( WC_Subscriptions_Synchroniser::is_syncing_enabled() && WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $subscription ) ) { $has_sync = true; foreach ( $subscription->get_items() as $item ) { $synced_payment_date = WC_Subscriptions_Synchroniser::calculate_first_payment_date( $item->get_product(), 'timestamp' ); // Check if the subscription starts from today because in those cases we don't need a dynamic trial period to align to the payment date. if ( WC_Subscriptions_Synchroniser::is_today( $synced_payment_date ) ) { $has_sync = false; break; } } } return $has_sync || $trial_end > time(); } /** * Gets the WC subscription associated with a WCPay subscription ID. * * @param string $wcpay_subscription_id WCPay subscription ID. * * @return WC_Subscription|bool The WC subscription or false if it can't be found. */ public static function get_subscription_from_wcpay_subscription_id( string $wcpay_subscription_id ) { if ( ! function_exists( 'wcs_get_subscriptions' ) ) { return false; } $subscriptions = wcs_get_subscriptions( [ 'subscriptions_per_page' => 1, 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query [ 'key' => self::SUBSCRIPTION_ID_META_KEY, 'value' => $wcpay_subscription_id, ], ], ] ); return empty( $subscriptions ) ? false : reset( $subscriptions ); } /** * Gets the WCPay subscription ID from a WC subscription. * * @param WC_Subscription $subscription WC Subscription. * * @return string */ public static function get_wcpay_subscription_id( WC_Subscription $subscription ) { return $subscription->get_meta( self::SUBSCRIPTION_ID_META_KEY, true ); } /** * Gets the WCPay subscription item ID from a WC subscription item. * * @param WC_Order_Item $item WC Item. * * @return string */ public static function get_wcpay_subscription_item_id( WC_Order_Item $item ) { return $item->get_meta( self::SUBSCRIPTION_ITEM_ID_META_KEY, true ); } /** * Gets the WCPay subscription discount IDs from a WC subscription. * * @param WC_Subscription $subscription WC Subscription. * * @return array */ public static function get_wcpay_discount_ids( WC_Subscription $subscription ) { return $subscription->get_meta( self::SUBSCRIPTION_DISCOUNT_IDS_META_KEY, true ); } /** * Sets Stripe discount ids on WC subscription. * * @param WC_Subscription $subscription The WC Subscription object. * @param array $discounts The WCPay discount data. * * @return void */ public static function set_wcpay_discount_ids( WC_Subscription $subscription, array $discounts ) { $subscription->update_meta_data( self::SUBSCRIPTION_DISCOUNT_IDS_META_KEY, $discounts ); $subscription->save(); } /** * Determines if a given WC subscription is a WCPay subscription. * * On duplicate sites (staging or dev environments) all WCPay Subscrptions are disabled and so return false. * This is to avoid dev environments interacting with WCPay Subscriptions and communicating on behalf of the live store. * * @param WC_Subscription $subscription WC Subscription object. * * @return bool */ public static function is_wcpay_subscription( WC_Subscription $subscription ): bool { return ! WC_Payments_Subscriptions::is_duplicate_site() && WC_Payment_Gateway_WCPay::GATEWAY_ID === $subscription->get_payment_method() && (bool) self::get_wcpay_subscription_id( $subscription ); } /** * Formats item data. * * @param string $currency The item's currency. * @param string $wcpay_product_id The item's Stripe product id. * @param float $unit_amount The item's unit amount. * @param string $interval The item's interval. Optional. * @param int $interval_count The item's interval count. Optional. * * @return array Structured invoice item array. */ public static function format_item_price_data( string $currency, string $wcpay_product_id, float $unit_amount, string $interval = '', int $interval_count = 0 ): array { $data = [ 'currency' => $currency, 'product' => $wcpay_product_id, // We cannot use WC_Payments_Utils::prepare_amount() here because it returns an int but 'unit_amount_decimal' supports multiple decimal places even though it is cents (fractions of a cent). 'unit_amount_decimal' => round( $unit_amount, wc_get_rounding_precision() ), ]; // Convert the amount to cents if it's not in a zero based currency. if ( ! WC_Payments_Utils::is_zero_decimal_currency( strtolower( $currency ) ) ) { $data['unit_amount_decimal'] *= 100; } if ( $interval && $interval_count ) { $data['recurring'] = [ 'interval' => $interval, 'interval_count' => $interval_count, ]; } return $data; } /** * Prepares discount data used to create a WCPay subscription. * * @param WC_Subscription $subscription The WC subscription used to create the subscription on server. * * @return array WCPay discount item data. */ public static function get_discount_item_data_for_subscription( WC_Subscription $subscription ): array { $data = []; foreach ( $subscription->get_items( 'coupon' ) as $item ) { $code = $item->get_code(); $coupon = new WC_Coupon( $code ); $duration = in_array( $coupon->get_discount_type(), [ 'recurring_fee', 'recurring_percent' ], true ) ? 'forever' : 'once'; $discount = $item->get_discount(); if ( $discount ) { $data[] = [ 'amount_off' => WC_Payments_Utils::prepare_amount( $discount, $subscription->get_currency() ), 'currency' => $subscription->get_currency(), 'duration' => $duration, // Translators: %s Coupon code. 'name' => sprintf( __( 'Coupon - %s', 'woocommerce-payments' ), $code ), ]; } } return $data; } /** * Gets a WCPay subscription from a WC subscription object. * * @param WC_Subscription $subscription The WC subscription to get from server. * * @return array|bool WCPay subscription data, otherwise false. */ public function get_wcpay_subscription( WC_Subscription $subscription ) { $wcpay_subscription_id = self::get_wcpay_subscription_id( $subscription ); if ( ! $wcpay_subscription_id ) { return false; } try { return $this->payments_api_client->get_subscription( $wcpay_subscription_id ); } catch ( API_Exception $e ) { return false; } } /** * Creates a WCPay subscription. * * @param WC_Subscription $subscription The WC order used to create a wcpay subscription on server. * * @return void * * @throws Exception Throws an exception to stop checkout processing and display message to customer. */ public function create_subscription( WC_Subscription $subscription ) { /* * Bail early if the subscription payment method is not WooPayments. * WCPay Subscriptions are not created in the following scenarios: * * - A different payment gateway was used to purchase the subscription (e.g. PayPal). * - The subscription is free (i.e. $0) and payment details were not captured during checkout. */ if ( WC_Payment_Gateway_WCPay::GATEWAY_ID !== $subscription->get_payment_method() ) { return; } $checkout_error_message = __( 'There was a problem creating your subscription. Please try again or contact us for assistance.', 'woocommerce-payments' ); $wcpay_customer_id = $this->customer_service->get_customer_id_for_order( $subscription ); if ( ! $wcpay_customer_id ) { Logger::error( 'There was a problem creating the WooPayments subscription. WooPayments customer ID missing.' ); throw new Exception( $checkout_error_message ); } try { $subscription_data = $this->prepare_wcpay_subscription_data( $wcpay_customer_id, $subscription ); $this->validate_subscription_data( $subscription_data ); $subscription_data['metadata']['subscription_source'] = $this->is_subscriptions_plugin_active() ? 'woo_subscriptions' : 'wcpay_subscriptions'; $response = $this->payments_api_client->create_subscription( $subscription_data ); $this->set_wcpay_subscription_id( $subscription, $response['id'] ); $this->set_wcpay_subscription_item_ids( $subscription, $response['items']['data'] ); if ( isset( $response['discounts'] ) ) { static::set_wcpay_discount_ids( $subscription, $response['discounts'] ); } if ( ! empty( $response['latest_invoice'] ) ) { $this->invoice_service->set_subscription_invoice_id( $subscription, $response['latest_invoice'] ); } } catch ( \Exception $e ) { Logger::log( sprintf( 'There was a problem creating the WooPayments subscription. %s', $e->getMessage() ) ); if ( $e instanceof Amount_Too_Small_Exception ) { throw new Exception( sprintf( // Translators: The %1 placeholder is a currency formatted price string ($0.50). The %2 and %3 placeholders are opening and closing strong HTML tags. __( 'There was a problem creating your subscription. %1$s doesn\'t meet the %2$sminimum recurring amount%3$s this payment method can process.', 'woocommerce-payments' ), wc_price( $subscription->get_total() ), '<strong>', '</strong>' ) ); } elseif ( $e instanceof Cannot_Combine_Currencies_Exception ) { throw new Exception( sprintf( // Translators: %1$s and %2$s are both currency codes, e.g. `USD` or `EUR`. __( 'The subscription couldn\'t be created because it uses a different currency (%1$s) from your existing subscriptions (%2$s). Please ensure all subscriptions use the same currency.', 'woocommerce-payments' ), $subscription->get_currency(), $e->get_currency() ) ); } throw new Exception( $checkout_error_message ); } } /** * Conditionally creates a WCPay subscription when a subscriber * updates the subscription payment method from their account page. * * @param WC_Subscription $subscription An instance of a WC_Subscription object. * @param string $new_payment_method The ID of the new payment method. * * @return void */ public function maybe_create_subscription_from_update_payment_method( WC_Subscription $subscription, string $new_payment_method ) { // Not changing the subscription payment method to WooPayments, bail. if ( WC_Payment_Gateway_WCPay::GATEWAY_ID !== $new_payment_method ) { return; } // We already have a WCPay subscription ID, bail. if ( (bool) self::get_wcpay_subscription_id( $subscription ) ) { return; } $this->is_creating_subscription_from_update_payment_method = true; $this->create_subscription( $subscription ); } /** * Cancels the WCPay subscription when it's cancelled in WC. * * @param WC_Subscription $subscription The WC subscription that was canceled. * * @return void */ public function cancel_subscription( WC_Subscription $subscription ) { $wcpay_subscription_id = self::get_wcpay_subscription_id( $subscription ); if ( ! $wcpay_subscription_id ) { return; } try { $this->payments_api_client->cancel_subscription( $wcpay_subscription_id ); } catch ( API_Exception $e ) { Logger::log( sprintf( 'There was a problem canceling the subscription on WooPayments server: %s.', $e->getMessage() ) ); } } /** * Handle subscription status change to on-hold. * * @param WC_Subscription $subscription The WC subscription. * * @return void */ public function handle_subscription_status_on_hold( WC_Subscription $subscription ) { // Check if the subscription is a WCPay subscription before proceeding. // In stores that have WC Subscriptions active, or previously had WC S, // this method may be called with regular tokenised subscriptions. if ( ! static::is_wcpay_subscription( $subscription ) ) { return; } $this->suspend_subscription( $subscription ); // Add an order note as a visible record of suspend. $subscription->add_order_note( __( 'Suspended WooPayments Subscription because subscription status changed to on-hold.', 'woocommerce-payments' ) ); // Log that the subscription was suspended. // Include a brief stack trace to help determine where status change originated. // For example, admin user action, or a code interaction with customizations. $e = new Exception(); $trace = $e->getTraceAsString(); Logger::log( sprintf( 'Suspended WooPayments Subscription because subscription status changed to on-hold. WC ID: %d; WooPayments ID: %s; stack: %s', $subscription->get_id(), self::get_wcpay_subscription_id( $subscription ), $trace ) ); } /** * Suspends a WCPay subscription. * * @param WC_Subscription $subscription The WC subscription to suspend. * * @return void */ public function suspend_subscription( WC_Subscription $subscription ) { // Check if the subscription is a WCPay subscription before proceeding. if ( ! static::is_wcpay_subscription( $subscription ) ) { Logger::log( sprintf( 'Aborting WC_Payments_Subscription_Service::suspend_subscription; subscription is a tokenised (non WooPayments) subscription. WC ID: %d.', $subscription->get_id() ) ); return; } $this->update_subscription( $subscription, [ 'pause_collection' => [ 'behavior' => 'void' ] ] ); } /** * Reactivates the WCPay subscription when the WC subscription is activated. * This is done by making a request to server to unset the "cancellation at end of period" value for the WooPayments subscription. * * @param WC_Subscription $subscription The WC subscription that was activated. * * @return void */ public function reactivate_subscription( WC_Subscription $subscription ) { $this->update_subscription( $subscription, [ 'cancel_at_period_end' => 'false', 'pause_collection' => '', ] ); } /** * Marks the WCPay subscription as pending-cancel by setting the "cancellation at end of period" on the WooPayments subscription. * * @param WC_Subscription $subscription The subscription that was set as pending cancel. * * @return void */ public function set_pending_cancel_for_subscription( WC_Subscription $subscription ) { $this->update_subscription( $subscription, [ 'cancel_at_period_end' => 'true' ] ); } /** * When a WC Subscription's payment method has been updated make sure we attach * the new payment method ID to the WCPay subscription. * * If the WCPay subscription's payment method was updated while there's a failed invoice, trigger a retry. * * @param int $subscription_id Post ID (WC subscription ID) that had its payment method updated. * @param int $token_id Payment Token post ID stored in DB. * @param WC_Payment_Token $token Payment Token object. */ public function update_wcpay_subscription_payment_method( int $subscription_id, int $token_id, WC_Payment_Token $token ) { if ( ! function_exists( 'wcs_get_subscription' ) ) { return; } $subscription = wcs_get_subscription( $subscription_id ); if ( $subscription && self::is_wcpay_subscription( $subscription ) ) { $wcpay_subscription_id = static::get_wcpay_subscription_id( $subscription ); $wcpay_payment_method_id = $token->get_token(); if ( $wcpay_subscription_id && $wcpay_payment_method_id ) { try { $this->update_subscription( $subscription, [ 'default_payment_method' => $wcpay_payment_method_id ] ); } catch ( API_Exception $e ) { Logger::error( sprintf( 'There was a problem updating the WooPayments subscription\'s default payment method on server: %s.', $e->getMessage() ) ); return; } } } } /** * Attempts payment for WCPay subscription if needed. * * @param WC_Subscription $subscription WC subscription linked to the WCPay subscription that maybe needs to retry payment. * @param WC_Payment_Token $token The new subscription token to assign to the invoice order. * * @return void */ public function maybe_attempt_payment_for_subscription( $subscription, WC_Payment_Token $token ) { if ( ! function_exists( 'wcs_is_subscription' ) || ! wcs_is_subscription( $subscription ) ) { return; } $wcpay_invoice_id = WC_Payments_Invoice_Service::get_pending_invoice_id( $subscription ); if ( ! $wcpay_invoice_id || ! self::is_wcpay_subscription( $subscription ) ) { return; } $response = $this->payments_api_client->charge_invoice( $wcpay_invoice_id ); // Rather than wait for the Stripe webhook to be received, complete the order now if it was successfully paid. if ( $response && isset( $response['status'] ) && 'paid' === $response['status'] ) { // Remove the pending invoice ID now that we know it has been paid. $this->invoice_service->mark_pending_invoice_paid_for_subscription( $subscription ); $order_id = WC_Payments_Invoice_Service::get_order_id_by_invoice_id( $wcpay_invoice_id ); $order = $order_id ? wc_get_order( $order_id ) : false; if ( $order && $order->needs_payment() ) { // We're about to record a successful payment, temporarily remove the "is request to change payment method" flag as it prevents us from activating the subscrption via WC_Subscription::payment_complete(). $is_change_payment_request = WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment; WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment = false; // We need to store the successful token on the order otherwise WC_Subscriptions_Change_Payment_Gateway::change_failing_payment_method() will override the successful token with the failing one. $order->add_payment_token( $token ); $order->payment_complete(); // Reinstate the "is request to change payment method" flag. WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment = $is_change_payment_request; wc_add_notice( __( "We've successfully collected payment for your subscription using your new payment method.", 'woocommerce-payments' ) ); } } } /** * Whether the subscription supports a given feature. * * @param bool $supported Is feature supported. * @param string $feature Feature flag. * @param WC_Subscription $subscription WC Subscription to check if feature is supported against. * * @return bool */ public function prevent_wcpay_subscription_changes( bool $supported, string $feature, WC_Subscription $subscription ) { $is_stripe_billing = self::is_wcpay_subscription( $subscription ); switch ( $feature ) { case 'subscription_amount_changes': case 'subscription_date_changes': $supported = ! $is_stripe_billing; break; case 'gateway_scheduled_payments': $supported = $is_stripe_billing; break; } if ( $is_stripe_billing ) { $supported = in_array( $feature, $this->supports, true ) || isset( $this->feature_support_exceptions[ $subscription->get_id() ][ $feature ] ); } return $supported; } /** * Remove pending parent and renewal order creation from admin edit subscriptions page. * * @param array $actions Array of available actions. * @return array Array of updated actions. */ public function prevent_wcpay_manual_renewal( array $actions ) { global $theorder; if ( ! function_exists( 'wcs_is_subscription' ) || ! $theorder ) { return $actions; } if ( wcs_is_subscription( $theorder ) && self::is_wcpay_subscription( $theorder ) ) { unset( $actions['wcs_create_pending_parent'], $actions['wcs_create_pending_renewal'], $actions['wcs_process_renewal'] ); } return $actions; } /** * Show WCPay Subscription ID on Edit Subscription page. * * @param WC_Order|WC_Subscription $order The order object. */ public function show_wcpay_subscription_id( WC_Order $order ) { if ( ! function_exists( 'wcs_is_subscription' ) || ! wcs_is_subscription( $order ) || ! self::is_wcpay_subscription( $order ) ) { return; } $wcpay_subscription_id = self::get_wcpay_subscription_id( $order ); if ( ! $wcpay_subscription_id ) { return; } echo '<p><strong>' . sprintf( /* translators: %s: WooPayments */ esc_html__( '%s Subscription ID', 'woocommerce-payments' ), 'WooPayments' ) . ':</strong> ' . esc_html( $wcpay_subscription_id ) . '</p>'; } /** * Updates a subscription's next payment date to match the WooPayments subscription's payment date. * * @param array $wcpay_subscription The WCPay Subscription data. * @param WC_Subscription $subscription The WC Subscription object. * * @return void */ public function update_dates_to_match_wcpay_subscription( array $wcpay_subscription, WC_Subscription $subscription ) { // Temporarily allow date changes when we're updating dates to match the dates on the WooPayments subscription. $this->set_feature_support_exception( $subscription, 'subscription_date_changes' ); $next_payment_date = gmdate( 'Y-m-d H:i:s', $wcpay_subscription['current_period_end'] ); $subscription->update_dates( [ 'next_payment' => $next_payment_date ] ); $next_payment_time_difference = absint( $wcpay_subscription['current_period_end'] - $subscription->get_time( 'next_payment' ) ); if ( $next_payment_time_difference > 0 && $next_payment_time_difference >= 12 * HOUR_IN_SECONDS ) { $subscription->add_order_note( __( 'The subscription\'s next payment date has been updated to match WooPayments server.', 'woocommerce-payments' ) ); } // Remove the 'subscription_date_changes' exception. $this->clear_feature_support_exception( $subscription, 'subscription_date_changes' ); } /** * Creates a WCPay subscription on successful renewal payment for manual WC subscription. * * @param int $order_id WC Order ID. */ public function create_subscription_for_manual_renewal( int $order_id ) { if ( ! function_exists( 'wcs_get_subscriptions_for_renewal_order' ) ) { return; } $subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id ); foreach ( $subscriptions as $subscription ) { if ( ! self::get_wcpay_subscription_id( $subscription ) && $subscription->is_manual() ) { // Only create WCPay subscription if subscription has payment tokens (reusable payment methods). $payment_tokens = $subscription->get_payment_tokens(); if ( ! empty( $payment_tokens ) ) { $this->create_subscription( $subscription ); } // If no payment tokens, don't create WCPay subscription (non-reusable payment methods). } } } /** * Prepares item data used to create a WCPay subscription. * * @param string $wcpay_customer_id WCPay Customer ID to create the subscription for. * @param WC_Subscription $subscription The WC subscription used to create the subscription on server. * * @return array WCPay subscription data */ private function prepare_wcpay_subscription_data( string $wcpay_customer_id, WC_Subscription $subscription ) { $recurring_items = $this->get_recurring_item_data_for_subscription( $subscription ); $one_time_items = $this->get_one_time_item_data_for_subscription( $subscription ); $discount_items = self::get_discount_item_data_for_subscription( $subscription ); $data = [ 'customer' => $wcpay_customer_id, 'items' => $recurring_items, ]; if ( self::has_delayed_payment( $subscription ) ) { $data['trial_end'] = max( $subscription->get_time( 'trial_end' ), $subscription->get_time( 'next_payment' ) ); } if ( ! empty( $one_time_items ) ) { $data['add_invoice_items'] = $one_time_items; } if ( ! empty( $discount_items ) ) { $data['discounts'] = $discount_items; } if ( $this->is_creating_subscription_from_update_payment_method ) { $data['backdate_start_date'] = max( $subscription->get_time( 'start' ), $subscription->get_time( 'last_order_date_created' ), $subscription->get_time( 'last_order_date_paid' ) ); $data['billing_cycle_anchor'] = $subscription->get_time( 'next_payment' ); } return apply_filters( 'wcpay_subscriptions_prepare_subscription_data', $data ); } /** * Gets recurring item data from a subscription needed to create a WCPay subscription. * * @param WC_Subscription $subscription The WC subscription to fetch product data from. * * @return array WCPay recurring item data. */ public function get_recurring_item_data_for_subscription( WC_Subscription $subscription ): array { $data = []; foreach ( $subscription->get_items() as $item ) { $data[] = [ 'metadata' => $this->get_item_metadata( $item ), 'quantity' => $item->get_quantity(), 'price_data' => static::format_item_price_data( $subscription->get_currency(), $this->product_service->get_or_create_wcpay_product_id( $item->get_product() ), $item->get_subtotal() / $item->get_quantity(), $subscription->get_billing_period(), $subscription->get_billing_interval() ), ]; } $additional_items = array_merge( $subscription->get_fees(), $subscription->get_shipping_methods(), $subscription->get_taxes() ); foreach ( $additional_items as $item ) { if ( is_a( $item, 'WC_Order_Item_Tax' ) ) { $item_name = $item->get_label(); $unit_amount = $item->get_tax_total() + $item->get_shipping_tax_total(); } else { $item_name = $item->get_type(); $unit_amount = $item->get_total(); } if ( $unit_amount ) { $data[] = [ 'metadata' => $this->get_item_metadata( $item ), 'price_data' => self::format_item_price_data( $subscription->get_currency(), $this->product_service->get_wcpay_product_id_for_item( $item_name ), $unit_amount, $subscription->get_billing_period(), $subscription->get_billing_interval() ), ]; } } return $data; } /** * Cancels a WCPay subscription when a customer changes their payment method * * @param WC_Subscription $subscription The subscription that was updated. * @param string $new_payment_method The subscription's new payment method ID. */ public function maybe_cancel_subscription( $subscription, $new_payment_method ) { $wcpay_subscription_id = self::get_wcpay_subscription_id( $subscription ); if ( (bool) $wcpay_subscription_id && WC_Payment_Gateway_WCPay::GATEWAY_ID !== $new_payment_method ) { $this->cancel_subscription( $subscription ); // Delete the WCPay Subscription meta but keep a record of it. $subscription->update_meta_data( '_cancelled' . self::SUBSCRIPTION_ID_META_KEY, $wcpay_subscription_id ); $subscription->delete_meta_data( self::SUBSCRIPTION_ID_META_KEY ); $subscription->save(); } } /** * Checks if the original subscription mode matches current WooPayments mode. * * If the original subscription was payed with WooPayments, but in the mode, that doesn't * match the current WooPayments mode, we need to throw an exception, to prevent the renewal * order from being created, as it would fail to be paid. * * @param array $items The items to be added to the renewal order. * @param WC_Order $order Renewal order. * @param WC_Subscription $subscription The original subscription. * @throws Subscription_Mode_Mismatch_Exception * @return array */ public function check_wcpay_mode_for_subscription( array $items, WC_Order $order, WC_Subscription $subscription ): array { $parent_order = $subscription->get_parent(); if ( false !== $parent_order ) { $subscription_mode = $parent_order->get_meta( WC_Payments_Order_Service::WCPAY_MODE_META_KEY ); $current_mode = WC_Payments::mode()->is_test() ? Order_Mode::TEST : Order_Mode::PRODUCTION; if ( is_string( $subscription_mode ) && '' !== $subscription_mode && $subscription_mode !== $current_mode ) { if ( Order_Mode::TEST === $subscription_mode ) { throw new Subscription_Mode_Mismatch_Exception( __( 'Subscription was made when WooPayments was in the test mode and cannot be renewed in the live mode.', 'woocommerce-payments' ) ); } else { throw new Subscription_Mode_Mismatch_Exception( __( 'Subscription was made when WooPayments was in the live mode and cannot be renewed in the test mode.', 'woocommerce-payments' ) ); } } } return $items; } /** * Gets one time item data from a subscription needed to create a WCPay subscription. * * @param WC_Subscription $subscription The WC subscription to fetch item data from. * * @return array WCPay one time item data. */ private function get_one_time_item_data_for_subscription( WC_Subscription $subscription ): array { $data = []; $currency = $subscription->get_currency(); foreach ( $subscription->get_items() as $item ) { $product = $item->get_product(); $sign_up_fee = (float) WC_Subscriptions_Product::get_sign_up_fee( $product ); $one_time_shipping = WC_Subscriptions_Product::needs_one_time_shipping( $product ); if ( $sign_up_fee ) { $wcpay_item_id = $this->product_service->get_wcpay_product_id_for_item( 'sign_up_fee' ); $data[] = [ 'price_data' => self::format_item_price_data( $currency, $wcpay_item_id, $sign_up_fee ), ]; } if ( $one_time_shipping ) { $wcpay_item_id = $this->product_service->get_wcpay_product_id_for_item( 'shipping' ); $shipping = 0; foreach ( $subscription->get_parent()->get_shipping_methods() as $shipping_method ) { $shipping += $shipping_method->get_total(); } $data[] = [ 'price_data' => self::format_item_price_data( $currency, $wcpay_item_id, $shipping ), ]; } } return $data; } /** * Updates a WCPay subscription. * * @param WC_Subscription $subscription The WC subscription that relates to the WCPay subscription that needs updating. * @param array $data Data to update. * * @return array|null Updated wcpay subscription or null if there was an error. */ private function update_subscription( WC_Subscription $subscription, array $data ) { $wcpay_subscription_id = static::get_wcpay_subscription_id( $subscription ); $response = null; if ( ! $wcpay_subscription_id ) { return; } try { $response = $this->payments_api_client->update_subscription( $wcpay_subscription_id, $data ); } catch ( API_Exception $e ) { Logger::log( sprintf( 'There was a problem updating the WooPayments subscription on server: %s', $e->getMessage() ) ); } return $response; } /** * Set the trial end date for the WCPay subscription (this updates both trial end as well as next payment). * * @param WC_Subscription $subscription WC subscription linked to the WCPay subscription that needs updating. * @param int $timestamp Next payment or trial end timestamp in UTC. * * @return void */ private function set_trial_end_for_subscription( WC_Subscription $subscription, int $timestamp ) { $trial_end = 0 === $timestamp ? 'now' : $timestamp; $this->update_subscription( $subscription, [ 'trial_end' => $trial_end ] ); } /** * Sets the WCPay subscription ID meta for WC subscription. * * @param WC_Subscription $subscription WC Subscription to store meta against. * @param string $value WCPay subscription ID meta value. * * @return void */ private function set_wcpay_subscription_id( WC_Subscription $subscription, string $value ) { $subscription->update_meta_data( self::SUBSCRIPTION_ID_META_KEY, $value ); $subscription->save(); } /** * Sets Stripe subscription item ids on WC order items. * * @param WC_Subscription $subscription The WC Subscription object. * @param array $subscription_items The WCPay Subscription data. * * @return void */ private function set_wcpay_subscription_item_ids( WC_Subscription $subscription, array $subscription_items ) { foreach ( $subscription_items as $item ) { $wcpay_subscription_item_id = $item['id']; $subscription_item_id = isset( $item['metadata']['wc_item_id'] ) ? $item['metadata']['wc_item_id'] : false; if ( $subscription_item_id ) { $subscription_item = $subscription->get_item( $subscription_item_id ); $subscription_item->update_meta_data( self::SUBSCRIPTION_ITEM_ID_META_KEY, $wcpay_subscription_item_id ); $subscription_item->save(); } else { Logger::log( sprintf( // Translators: %s Stripe subscription item ID. __( 'Unable to set subscription item ID meta for WooPayments subscription item %s.', 'woocommerce-payments' ), $wcpay_subscription_item_id ) ); } } } /** * Temporarily allows a subscription to bypass a payment gateway feature support flag. * * Use @see WC_Payments_Subscription_Service::clear_feature_support_exception() to clear it. * * @param WC_Subscription $subscription The subscription to set the exception for. * @param string $feature The feature to allow. */ private function set_feature_support_exception( WC_Subscription $subscription, string $feature ) { $this->feature_support_exceptions[ $subscription->get_id() ][ $feature ] = true; } /** * Clears a gateway support flag exception. * * Use @see WC_Payments_Subscription_Service::set_feature_support_exception() to set one. * * @param WC_Subscription $subscription The subscription to remove the exception for. * @param string $feature The feature. */ private function clear_feature_support_exception( WC_Subscription $subscription, string $feature ) { unset( $this->feature_support_exceptions[ $subscription->get_id() ][ $feature ] ); } /** * Generates the metadata for a given WC_Order_Item * * @param WC_Order_Item|WC_Order_Item_Tax $item The order item to generate the metadata for. Can be any order item type including tax, shipping and fees. * @return array Item metadata. */ private function get_item_metadata( WC_Order_Item $item ) { $metadata = [ 'wc_item_id' => $item->get_id() ]; switch ( $item->get_type() ) { case 'tax': $metadata['wc_rate_id'] = $item->get_rate_id(); $metadata['code'] = $item->get_rate_code(); $metadata['rate'] = $item->get_rate_percent(); $metadata['is_compound'] = wc_bool_to_string( $item->is_compound() ); break; case 'shipping': $metadata['method'] = $item->get_name(); break; case 'fee': $metadata['type'] = $item->get_name(); break; } return $metadata; } /** * Validates that the data used to create the WCPay Subscription. * * @param array $subscription_data The data used to create a WCPay subscription. * @throws Exception If the subscription data contains invalid or missing data. */ private function validate_subscription_data( $subscription_data ) { if ( empty( $subscription_data['customer'] ) ) { throw new Exception( 'The "customer" arg is required to create the subscription.' ); } if ( ! isset( $subscription_data['items'] ) ) { throw new Exception( 'The "items" arg is required to create the subscription.' ); } foreach ( $subscription_data['items'] as $item_data ) { $required_price_keys = [ 'currency', 'product', 'recurring' ]; $required_period_keys = [ 'interval', 'interval_count' ]; $errors = []; if ( ! isset( $item_data['price_data']['unit_amount_decimal'] ) ) { $errors[] = 'unit_amount_decimal'; } foreach ( $required_price_keys as $required_key ) { if ( empty( $item_data['price_data'][ $required_key ] ) ) { $errors[] = $required_key; } } foreach ( $required_period_keys as $required_price_key ) { if ( empty( $item_data['price_data']['recurring'][ $required_price_key ] ) ) { $errors[] = $required_price_key; } } if ( ! empty( $errors ) ) { $error_message = count( $errors ) > 1 ? 'The "%s" line item properties are required to create the subscription.' : 'The "%s" line item property is required to create the subscription.'; throw new Exception( sprintf( $error_message, implode( '", "', $errors ) ) ); } $billing_period = $item_data['price_data']['recurring']['interval']; $billing_interval = $item_data['price_data']['recurring']['interval_count']; // Confirm the billing period is valid (no greater than 1 year in length). if ( ! $this->product_service->is_valid_billing_cycle( $billing_period, $billing_interval ) ) { throw new Exception( sprintf( 'The subscription billing period cannot be any longer than one year. A billing period of "every %s %s(s)" was given.', $billing_interval, $billing_period ) ); } } } /** * Determines if the store has any active WCPay subscriptions. * * @return bool True if store has active WCPay subscriptions, otherwise false. */ public static function store_has_active_wcpay_subscriptions() { if ( ! function_exists( 'wcs_get_subscriptions' ) ) { return false; } $active_wcpay_subscriptions = wcs_get_subscriptions( [ 'subscriptions_per_page' => 1, 'subscription_status' => 'active', // Ignoring phpcs warning, we need to search meta. 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query [ 'key' => self::SUBSCRIPTION_ID_META_KEY, 'compare' => 'EXISTS', ], ], ] ); return ( is_countable( $active_wcpay_subscriptions ) ? count( $active_wcpay_subscriptions ) : 0 ) > 0; } } includes/subscriptions/class-wc-payments-subscriptions-event-handler.php 0000777 00000035713 15251156605 0023031 0 ustar 00 <?php /** * Class WC_Payments_Subscriptions_Event_Handler * * @package WooCommerce\Payments */ use WCPay\Core\Server\Request\Get_Charge; use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\Invalid_Webhook_Data_Exception; use WCPay\Exceptions\Order_Not_Found_Exception; use WCPay\Logger; /** * Subscriptions Event/Webhook Handler class */ class WC_Payments_Subscriptions_Event_Handler { /** * Maximum amount of payment retries to handle before cancelling the subscription. * * @var int */ const MAX_RETRIES = 4; /** * Invoice Service. * * @var WC_Payments_Invoice_Service */ private $invoice_service; /** * Subscription Service. * * @var WC_Payments_Subscription_Service */ private $subscription_service; /** * Subscriptions event handler constructor. * * @param WC_Payments_Invoice_Service $invoice_service Invoice service. * @param WC_Payments_Subscription_Service $subscription_service Subscription service. */ public function __construct( WC_Payments_Invoice_Service $invoice_service, WC_Payments_Subscription_Service $subscription_service ) { $this->invoice_service = $invoice_service; $this->subscription_service = $subscription_service; } /** * Validate and correct subscription status, date, and lines. * * @param array $body The event body that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. */ public function handle_invoice_upcoming( array $body ) { $event_object = $this->get_event_property( $body, [ 'data', 'object' ] ); $wcpay_subscription_id = $this->get_event_property( $event_object, 'subscription' ); /** * When a store is in staging mode, we don't want any webhook handling response to be sent to the server. * * Sending requests from staging sites can have unintended consequences for the live store. For example, * Subscriptions which renew on the staging site will lead to paused subscriptions at Stripe and result in * missed renewal payments. */ if ( WC_Payments_Subscriptions::is_duplicate_site() ) { $this->log_skipped_webhook_due_to_staging( 'invoice.upcoming', $wcpay_subscription_id ); return; } $wcpay_discounts = $this->get_event_property( $event_object, 'discounts' ); $wcpay_lines = $this->get_event_property( $event_object, [ 'lines', 'data' ] ); $subscription = WC_Payments_Subscription_Service::get_subscription_from_wcpay_subscription_id( $wcpay_subscription_id ); if ( ! $subscription ) { throw new Invalid_Webhook_Data_Exception( __( 'Cannot find subscription to handle the "invoice.upcoming" event.', 'woocommerce-payments' ) ); } $wcpay_subscription = $this->subscription_service->get_wcpay_subscription( $subscription ); // Suspend or cancel subscription if we didn't expect a next payment. if ( 0 === $subscription->get_time( 'next_payment' ) ) { // TODO: Add error handling to these {cancel/suspend}_subscription calls i.e. add a subscription order note if the WCPay subscription wasn't cancelled. if ( ! $subscription->has_status( 'on-hold' ) && 0 !== $subscription->get_time( 'end' ) ) { $this->subscription_service->cancel_subscription( $subscription ); } else { $this->subscription_service->suspend_subscription( $subscription ); $subscription->add_order_note( __( 'Suspended WooPayments Subscription in invoice.upcoming webhook handler because subscription next_payment date is 0.', 'woocommerce-payments' ) ); Logger::log( sprintf( 'Suspended WooPayments Subscription in invoice.upcoming webhook handler because subscription next_payment date is 0. WC ID: %d; WooPayments ID: %s.', $subscription->get_id(), $wcpay_subscription_id ) ); } } else { // Translators: %s Scheduled/upcoming payment date in Y-m-d H:i:s format. $subscription->add_order_note( sprintf( __( 'Next automatic payment scheduled for %s.', 'woocommerce-payments' ), get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $wcpay_subscription['current_period_end'] ), wc_date_format() . ' ' . wc_time_format() ) ) ); $this->subscription_service->update_dates_to_match_wcpay_subscription( $wcpay_subscription, $subscription ); $this->invoice_service->validate_invoice( $wcpay_lines, $wcpay_discounts ? $wcpay_discounts : [], $subscription ); } } /** * Renews a subscription associated with paid invoice. * * @param array $body The event body that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. * @throws Order_Not_Found_Exception */ public function handle_invoice_paid( array $body ) { $event_data = $this->get_event_property( $body, 'data' ); $event_object = $this->get_event_property( $event_data, 'object' ); $wcpay_subscription_id = $this->get_event_property( $event_object, 'subscription' ); /** * When a store is in staging mode, we don't want any webhook handling response to be sent to the server. * * Sending requests from staging sites can have unintended consequences for the live store. For example, * Subscriptions which renew on the staging site will lead to paused subscriptions at Stripe and result in * missed renewal payments. */ if ( WC_Payments_Subscriptions::is_duplicate_site() ) { $this->log_skipped_webhook_due_to_staging( 'invoice.paid', $wcpay_subscription_id ); return; } $wcpay_invoice_id = $this->get_event_property( $event_object, 'id' ); $subscription = WC_Payments_Subscription_Service::get_subscription_from_wcpay_subscription_id( $wcpay_subscription_id ); if ( ! $subscription ) { throw new Invalid_Webhook_Data_Exception( __( 'Cannot find subscription for the incoming "invoice.paid" event.', 'woocommerce-payments' ) ); } // This incoming invoice.paid event is linked to the subscription parent invoice and can be ignored. if ( WC_Payments_Invoice_Service::get_subscription_invoice_id( $subscription ) === $wcpay_invoice_id ) { return; } $order = wc_get_order( WC_Payments_Invoice_Service::get_order_id_by_invoice_id( $wcpay_invoice_id ) ); if ( ! $order ) { $order = wcs_create_renewal_order( $subscription ); if ( is_wp_error( $order ) ) { throw new Invalid_Webhook_Data_Exception( __( 'Unable to generate renewal order for subscription on the "invoice.paid" event.', 'woocommerce-payments' ) ); } else { $order->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID ); $this->invoice_service->set_order_invoice_id( $order, $wcpay_invoice_id ); } } if ( $order->needs_payment() ) { /* * Temporarily place the subscription on-hold to imitate the normal subscription renewal flow. * This ensures the downstream effects take place, e.g. a payment status order note is added and the * 'woocommerce_subscription_payment_complete' action is fired. */ remove_action( 'woocommerce_subscription_status_on-hold', [ $this->subscription_service, 'handle_subscription_status_on_hold' ] ); $subscription->update_status( 'on-hold' ); add_action( 'woocommerce_subscription_status_on-hold', [ $this->subscription_service, 'handle_subscription_status_on_hold' ] ); /* * Remove the reactivate_subscription callback that occurs when a subscription transitions from on-hold to active. * The WCPay subscription will remain active throughout this process and does not need to be reactivated. */ remove_action( 'woocommerce_subscription_status_on-hold_to_active', [ $this->subscription_service, 'reactivate_subscription' ] ); $order->payment_complete(); add_action( 'woocommerce_subscription_status_on-hold_to_active', [ $this->subscription_service, 'reactivate_subscription' ] ); /** * Fetch a new instance of the subscription. * * After marking the order as paid, a parallel instance of the subscription would have been reactivated. * To avoid race conditions and cache pollution, fetch a new instance to ensure our current instance doesn't override the active subscription status. */ $subscription = wcs_get_subscription( $subscription->get_id() ); } if ( isset( $event_object['payment_intent'] ) ) { // Add the payment intent data to the order. $this->invoice_service->get_and_attach_intent_info_to_order( $order, $event_object['payment_intent'] ); } // Remove pending invoice ID in case one was recorded for previous failed renewal attempts. $this->invoice_service->mark_pending_invoice_paid_for_subscription( $subscription ); // Record the store's Stripe Billing environment context on the payment intent. $invoice = $this->invoice_service->record_subscription_payment_context( $wcpay_invoice_id ); // Update charge and transaction metadata - add order id for Stripe Billing. $this->invoice_service->update_charge_details( $invoice, $order->get_id() ); // Update transaction customer details for Stripe Billing. $this->invoice_service->update_transaction_details( $invoice, $order ); } /** * Marks a subscription payment associated with invoice as failed. * * @param array $body The event body that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. */ public function handle_invoice_payment_failed( array $body ) { $event_data = $this->get_event_property( $body, 'data' ); $event_object = $this->get_event_property( $event_data, 'object' ); $wcpay_subscription_id = $this->get_event_property( $event_object, 'subscription' ); /** * When a store is in staging mode, we don't want any webhook handling response to be sent to the server. * * Sending requests from staging sites can have unintended consequences for the live store. For example, * Subscriptions which renew on the staging site will lead to paused subscriptions at Stripe and result in * missed renewal payments. */ if ( WC_Payments_Subscriptions::is_duplicate_site() ) { $this->log_skipped_webhook_due_to_staging( 'invoice.payment_failed', $wcpay_subscription_id ); return; } $wcpay_invoice_id = $this->get_event_property( $event_object, 'id' ); $attempts = (int) $this->get_event_property( $event_object, 'attempt_count' ); $subscription = WC_Payments_Subscription_Service::get_subscription_from_wcpay_subscription_id( $wcpay_subscription_id ); if ( ! $subscription ) { throw new Invalid_Webhook_Data_Exception( __( 'Cannot find subscription for the incoming "invoice.payment_failed" event.', 'woocommerce-payments' ) ); } $charge = null; if ( isset( $event_object['charge'] ) ) { try { $charge = Get_Charge::create( $event_object['charge'] ); $charge = $charge->send(); } catch ( API_Exception $e ) { Logger::error( sprintf( 'Unable to retrieve charge data for invoice.payment_failed webhook. Charge ID: %s; Error: %s', $event_object['charge'], $e->getMessage() ) ); } } $error_details = ''; $error_code = ''; if ( $charge ) { if ( isset( $charge['outcome'] ) && isset( $charge['outcome']['seller_message'] ) ) { $error_details = $charge['outcome']['seller_message']; $error_code = $charge['failure_code']; } } $order = wc_get_order( WC_Payments_Invoice_Service::get_order_id_by_invoice_id( $wcpay_invoice_id ) ); if ( ! $order ) { $order = wcs_create_renewal_order( $subscription ); if ( is_wp_error( $order ) ) { throw new Invalid_Webhook_Data_Exception( __( 'Unable to generate renewal order for subscription to record the incoming "invoice.payment_failed" event.', 'woocommerce-payments' ) ); } else { $order->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID ); $this->invoice_service->set_order_invoice_id( $order, $wcpay_invoice_id ); } } if ( $error_details ) { $subscription->add_order_note( sprintf( // Translators: %1$d Number of failed renewal attempts. %2$s contains failure message, %3$s contains error code. _n( 'WooPayments subscription renewal attempt %1$d failed with the following message "%2$s" and failure code <code>%3$s</code>', 'WooPayments subscription renewal attempt %1$d failed with the following message "%2$s" and failure code <code>%3$s</code>', $attempts, 'woocommerce-payments' ), $attempts, $error_details, $error_code ) ); $order->add_order_note( sprintf( // Translators: %1$s contains failure message. %2$s contains error code. __( 'Payment for the order failed with the following message: "%1$s" and failure code <code>%2$s</code>', 'woocommerce-payments' ), $error_details, $error_code ) ); } else { // Translators: %d Number of failed renewal attempts. $subscription->add_order_note( sprintf( _n( 'WooPayments subscription renewal attempt %d failed.', 'WooPayments subscription renewal attempt %d failed.', $attempts, 'woocommerce-payments' ), $attempts ) ); } if ( self::MAX_RETRIES > $attempts ) { remove_action( 'woocommerce_subscription_status_on-hold', [ $this->subscription_service, 'handle_subscription_status_on_hold' ] ); $subscription->payment_failed(); add_action( 'woocommerce_subscription_status_on-hold', [ $this->subscription_service, 'handle_subscription_status_on_hold' ] ); } else { $subscription->payment_failed( 'cancelled' ); } // Record invoice ID so we can trigger repayment on payment method update. $this->invoice_service->mark_pending_invoice_for_subscription( $subscription, $wcpay_invoice_id ); // Record the store's Stripe Billing environment context on the payment intent. $this->invoice_service->record_subscription_payment_context( $wcpay_invoice_id ); } /** * Gets the event data by property. * * @param array $event_data Event data. * @param mixed $key Requested key. * * @return mixed * * @throws Invalid_Webhook_Data_Exception Event data not found by key. */ private function get_event_property( array $event_data, $key ) { $keys = is_array( $key ) ? $key : [ $key ]; $data = $event_data; foreach ( $keys as $k ) { if ( ! isset( $data[ $k ] ) ) { // Translators: %s Property name not found in event data array. throw new Invalid_Webhook_Data_Exception( sprintf( __( '%s not found in array', 'woocommerce-payments' ), $k ) ); } $data = $data[ $k ]; } return $data; } /** * Creates a log entry noting that a subscription-related webhook has been skipped due to the current site being in staging mode. * * @param string $event The webhook event type. eg "invoice.paid". * @param string $wcpay_subscription_id The WCPay subsciption ID. */ private function log_skipped_webhook_due_to_staging( string $event, string $wcpay_subscription_id ) { Logger::info( sprintf( // Example message: "invoice.paid webhook processing for sub_abc123defg456 was skipped. The current site (https://staging.example.com) is in staging mode. Live site is https://example.com. '%s webhook processing for %s was skipped. The current site (%s) is in staging mode. Live site is %s.', $event, $wcpay_subscription_id, WCS_Staging::get_site_url_from_source( 'current_wp_site' ), WCS_Staging::get_site_url_from_source( 'subscriptions_install' ) ) ); } } includes/subscriptions/class-wc-payments-subscriptions-onboarding-handler.php 0000777 00000021042 15251156605 0024020 0 ustar 00 <?php /** * Class WC_Payments_Subscriptions_Onboarding_Handler * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; use WCPay\Tracker; /** * A class to handle the onboarding of subscriptions products. Created subscription products will be set to draft until * onboarding is completed. */ class WC_Payments_Subscriptions_Onboarding_Handler { use WC_Payments_Subscriptions_Utilities; /** * Option for holding an array of product id's to publish post onboarding. * * @const string */ const WCPAY_SUBSCRIPTION_AUTO_PUBLISH_PRODUCTS = 'wcpay_subscription_onboarding_products'; /** * The account service instance. * * @var WC_Payments_Account */ private $account; /** * Constructor * * @param WC_Payments_Account $account account service instance. */ public function __construct( WC_Payments_Account $account ) { $this->account = $account; if ( ! WC_Payments_Features::should_use_stripe_billing() ) { return; } // This action is triggered on product save but after other required subscriptions logic is triggered. add_action( 'woocommerce_admin_process_product_object', [ $this, 'product_save' ] ); add_action( 'woocommerce_payments_account_refreshed', [ $this, 'account_data_refreshed' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_modal_scripts_and_styles' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_toast_script' ] ); add_filter( 'woocommerce_subscriptions_admin_pointer_script_parameters', [ $this, 'filter_admin_pointer_script_parameters' ] ); } /** * Sets the account service instance reference on the class. * * @param WC_Payments_Account $account account service instance. */ public function set_account( WC_Payments_Account $account ) { $this->account = $account; } /** * Convert subscriptions to drafts when using WCPay (without subscriptions) and onboarding is not complete. * This should be triggered just prior to a $product->save() call so no need to call product->save() * * @param WC_Product $product Subscriptions Product. */ public function product_save( WC_Product $product ) { if ( $this->account->is_stripe_connected() ) { return; } // If Subscriptions plugin is installed we don't need to do this check. if ( $this->is_subscriptions_plugin_active() ) { return; } // Skip products which have already been scheduled or aren't subscriptions. if ( ! WC_Subscriptions_Product::is_subscription( $product ) ) { return; } // We can skip if the post is not yet marked as published. if ( 'publish' !== $product->get_status() ) { return; } // Change the default WP saved post URL to correctly reflect the draft status and to add our saved-as-draft flag. add_filter( 'redirect_post_location', function () use ( $product ) { return add_query_arg( // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in. [ 'message' => 10, // Post saved as draft message. 'wcpay-subscription-saved-as-draft' => 1, ], get_edit_post_link( $product->get_id(), 'url' ) ); } ); $this->convert_subscription_to_draft( $product ); } /** * Convert a product to a draft and save the id in an array, so we can auto-publish once onboarding is complete. * * @param WC_Product $product Product to convert to draft. */ private function convert_subscription_to_draft( WC_Product $product ) { // Force into draft status. $product->set_status( 'draft' ); $product->save(); $auto_publish_ids = get_option( self::WCPAY_SUBSCRIPTION_AUTO_PUBLISH_PRODUCTS, [] ); $auto_publish_ids[] = $product->get_id(); // Save and prevent duplicates from multiple updates. update_option( self::WCPAY_SUBSCRIPTION_AUTO_PUBLISH_PRODUCTS, array_unique( $auto_publish_ids ) ); Tracker::track_admin( 'wcpay_subscriptions_account_not_connected_save_product' ); } /** * Method to handle when account data is refreshed and onboarding may have been completed */ public function account_data_refreshed() { if ( ! $this->account->is_stripe_connected() ) { return; } $products = get_option( self::WCPAY_SUBSCRIPTION_AUTO_PUBLISH_PRODUCTS, [] ); if ( [] === $products ) { return; } foreach ( $products as $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product || ! WC_Subscriptions_Product::is_subscription( $product ) ) { continue; } if ( 'draft' !== $product->get_status() ) { continue; } $product->set_status( 'publish' ); $product->save(); } // clear auto-published products from option. delete_option( self::WCPAY_SUBSCRIPTION_AUTO_PUBLISH_PRODUCTS ); } /** * Enqueues the admin scripts needed on the add/edit product screen when the * merchant attempts to publish a subscription product prior to completing * WCPay onboarding. * * @param string $hook_suffix The current admin page. */ public function enqueue_modal_scripts_and_styles( $hook_suffix ) { global $post; if ( ! in_array( $hook_suffix, [ 'post.php', 'post-new.php' ], true ) ) { return; } if ( ! $post || 'product' !== $post->post_type ) { return; } if ( empty( $_GET['wcpay-subscription-saved-as-draft'] ) || 1 !== (int) $_GET['wcpay-subscription-saved-as-draft'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } if ( $this->is_subscriptions_plugin_active() ) { return; } if ( $this->account->is_stripe_connected() ) { return; } WC_Payments::register_script_with_dependencies( 'wcpay-subscription-product-onboarding-modal', 'dist/subscription-product-onboarding-modal' ); wp_localize_script( 'wcpay-subscription-product-onboarding-modal', 'wcpaySubscriptionProductOnboardingModal', [ 'connectUrl' => WC_Payments_Account::get_connect_url( 'WC_SUBSCRIPTIONS_PUBLISH_PRODUCT_' . $post->ID ), 'pluginScope' => ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '6.5', '>=' ) ) ? 'woocommerce-admin' : 'woocommerce', ] ); WC_Payments_Utils::register_style( 'wcpay-subscription-product-onboarding-modal', plugins_url( 'dist/subscription-product-onboarding-modal.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'dist/subscription-product-onboarding-modal.css' ), 'all' ); wp_enqueue_script( 'wcpay-subscription-product-onboarding-modal' ); wp_enqueue_style( 'wcpay-subscription-product-onboarding-modal' ); } /** * Enqueues the admin scripts needed on the add/edit product screen when the * merchant has completed WCPay onboarding and is redirected back to product * edit page. * * @param string $hook_suffix The current admin page. */ public function enqueue_toast_script( $hook_suffix ) { global $post; if ( ! in_array( $hook_suffix, [ 'post.php', 'post-new.php' ], true ) ) { return; } if ( ! $post || 'product' !== $post->post_type ) { return; } if ( empty( $_GET['wcpay-subscriptions-onboarded'] ) || 1 !== (int) $_GET['wcpay-subscriptions-onboarded'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } if ( $this->is_subscriptions_plugin_active() ) { return; } WC_Payments::register_script_with_dependencies( 'wcpay-subscription-product-onboarding-toast', 'dist/subscription-product-onboarding-toast' ); wp_localize_script( 'wcpay-subscription-product-onboarding-toast', 'wcpaySubscriptionProductOnboardingToast', [ 'pluginScope' => ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '6.5', '>=' ) ) ? 'woocommerce-admin' : 'woocommerce', ] ); wp_enqueue_script( 'wcpay-subscription-product-onboarding-toast' ); } /** * Modifies the pointer content found on the "Add new product" page * when WooCommerce Subscriptions is not active. * * @param array $pointer_params Array of strings used on the "Add new product" page. * @return array Potentially modified array of strings used on the "Add new product" page. */ public function filter_admin_pointer_script_parameters( $pointer_params ) { if ( $this->is_subscriptions_plugin_active() ) { return $pointer_params; } // translators: %1$s: <h3> tag, %2$s: </h3> tag, %3$s: <p> tag, %4$s: WooPayments, %5$s: <em> tag, %6$s: </em> tag, %7$s: <em> tag, %8$s: </em> tag, %9$s: </p> tag. $pointer_params['typePointerContent'] = sprintf( _x( '%1$sChoose Subscription%2$s%3$s%4$s adds two new subscription product types - %5$sSimple subscription%6$s and %7$sVariable subscription%8$s.%9$s', 'used in admin pointer script params in javascript as type pointer content', 'woocommerce-payments' ), '<h3>', '</h3>', '<p>', 'WooPayments', '<em>', '</em>', '<em>', '</em>', '</p>' ); return $pointer_params; } } includes/subscriptions/assets/css/plugin-page.css 0000777 00000003603 15251156605 0016314 0 ustar 00 .woopayments-plugin-warning-modal .button { height: 36px; align-items: center; box-sizing: border-box; padding: 6px 12px; border-radius: 2px; line-height: normal; cursor: pointer; } .woopayments-plugin-warning-modal .button-secondary { background: transparent; } .woopayments-plugin-warning-modal .button.busy { animation: wcpay-plugin-notice-submit-button-busy 3000ms infinite linear; background-image: linear-gradient( -45deg, transparent 33%, #0000004d 33%, #0000004d 70%, transparent 70% ); background-size: 100px 100%; } .woopayments-plugin-warning-modal footer .inner > *:not( :first-child ) { margin-left: 16px; } .woopayments-plugin-warning-modal h1 { font-size: 1rem; font-weight: 600; line-height: 1; } .woopayments-plugin-warning-modal header, .woopayments-plugin-warning-modal footer { background: initial; box-shadow: initial; } .woopayments-plugin-warning-modal header { margin: 0 -24px; padding-top: 24px; padding-left: 24px; height: 60px; border-bottom: 1px solid #dcdcde; } .woopayments-plugin-warning-modal footer { padding: 24px; border-top: 1px solid #dcdcde; } .woopayments-plugin-warning-modal .wc-backbone-modal-content { border-radius: 2px; padding: 0 24px 24px; } .woopayments-plugin-warning-modal .modal-close-link { height: 100%; width: 60px; border-left: none; } .woopayments-plugin-warning-modal .modal-close-link:hover { background: transparent; } .woopayments-plugin-warning-modal article { padding: 1.5em 0; } .woopayments-plugin-warning-modal .wc-backbone-modal-main { padding-bottom: 60px; } .woopayments-plugin-warning-modal .modal-close-link:hover::before { color: initial; } .woopayments-plugin-warning-modal ul { padding-left: 24px; list-style: disc; } .woopayments-plugin-warning-modal ul > li > ul { margin-top: 6px; list-style: circle; } @keyframes wcpay-plugin-notice-submit-button-busy { 0% { background-position: 200px 0; } } includes/subscriptions/assets/js/plugin-page.js 0000777 00000005314 15251156605 0015765 0 ustar 00 jQuery( function ( $ ) { 'use strict'; var wc_payments_plugin = { init: function () { this.init_deactivate_wc_subscriptions_warning(); this.init_deactivate_wcpay_warning(); }, // Initialise handlers for WC Pay deactivate warning. init_deactivate_wcpay_warning() { // Intercept click on WCPay deactivate link to show modal. $( '#deactivate-woocommerce-payments' ).on( 'click', this.display_wcpay_warning ); // Resume deactivate when user confirms modal. $( document ).on( 'click', '#wcpay-plugin-deactivate-modal-submit', this.redirect_deactivate_wcpay ); }, // Show a modal to warn merchant that disabling WCPay plugin may leave Stripe subscriptions active (and renewing). display_wcpay_warning: function ( event ) { event.preventDefault(); $( this ).WCBackboneModal( { template: 'wcpay-plugin-deactivate-warning', } ); return false; }, // Trigger deactivate flow for WCPay. redirect_deactivate_wcpay: function ( event ) { $( '#wcpay-plugin-deactivate-modal-submit' ).addClass( 'busy' ); window.location = $( '#deactivate-woocommerce-payments' ).attr( 'href' ); }, // Initialise handlers for Woo Subscriptions deactivate warning. init_deactivate_wc_subscriptions_warning() { // Intercept click on Woo Subscriptions deactivate link to show modal. $( '#deactivate-' + this.get_woo_subscriptions_plugin_slug() ).on( 'click', this.display_wcs_warning ); // Resume deactivate when user confirms modal. $( document ).on( 'click', '#wcpay-subscriptions-plugin-deactivation-submit', this.redirect_deactivate_wc_subscriptions ); }, // Show a modal to warn merchant that disabling WC Subscriptions plugin will switch to WCPay. display_wcs_warning: function ( event ) { event.preventDefault(); $( this ).WCBackboneModal( { template: 'wcpay-subscriptions-plugin-warning', } ); return false; }, // Trigger deactivate flow for WC Subscriptions. redirect_deactivate_wc_subscriptions: function ( event ) { $( '#wcpay-subscriptions-plugin-deactivation-submit' ).addClass( 'busy' ); window.location = $( '#deactivate-' + wc_payments_plugin.get_woo_subscriptions_plugin_slug() ).attr( 'href' ); }, // Gets the Woo Subscriptions plugin slug. When the site is connected to WooCommerce.com, the slug is different and includes a woocommerce-com- prefix. get_woo_subscriptions_plugin_slug() { const element = document.querySelector( '[data-slug="woocommerce-com-woocommerce-subscriptions"]' ); if ( element ) { return 'woocommerce-com-woocommerce-subscriptions'; } else { return 'woocommerce-subscriptions'; } }, }; wc_payments_plugin.init(); } ); includes/subscriptions/class-wc-payments-subscriptions.php 0000777 00000013314 15251156605 0020270 0 ustar 00 <?php /** * Class WC_Payments_Subscriptions. * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Class for loading WooCommerce Payments Subscriptions. */ class WC_Payments_Subscriptions { /** * Instance of WC_Payments_Product_Service, created in init function. * * @var WC_Payments_Product_Service */ private static $product_service; /** * Instance of WC_Payments_Order_Service, created in init function. * * @var WC_Payments_Order_Service */ private static $order_service; /** * Instance of WC_Payments_Invoice_Service, created in init function. * * @var WC_Payments_Invoice_Service */ private static $invoice_service; /** * Instance of WC_Payments_Subscription_Service, created in init function. * * @var WC_Payments_Subscription_Service */ private static $subscription_service; /** * Instance of WC_Payments_Subscriptions_Event_Handler, created in init function. * * @var WC_Payments_Subscriptions_Event_Handler */ private static $event_handler; /** * Instance of WC_Payments_Subscriptions_Migrator, created in init function. * * @var WC_Payments_Subscriptions_Migrator */ private static $stripe_billing_migrator; /** * Initialize WooCommerce Payments subscriptions. (Stripe Billing) * * @param WC_Payments_API_Client $api_client WCPay API client. * @param WC_Payments_Customer_Service $customer_service WCPay Customer Service. * @param WC_Payments_Order_Service $order_service WCPay Order Service. * @param WC_Payments_Account $account WC_Payments_Account. * @param WC_Payments_Token_Service $token_service WC_Payments_Token_Service. */ public static function init( WC_Payments_API_Client $api_client, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service, WC_Payments_Account $account, WC_Payments_Token_Service $token_service ) { // Store dependencies. self::$order_service = $order_service; // Load Services. include_once __DIR__ . '/class-wc-payments-product-service.php'; include_once __DIR__ . '/class-wc-payments-invoice-service.php'; include_once __DIR__ . '/class-wc-payments-subscription-service.php'; include_once __DIR__ . '/class-wc-payments-subscription-change-payment-method-handler.php'; include_once __DIR__ . '/class-wc-payments-subscriptions-plugin-notice-manager.php'; include_once __DIR__ . '/class-wc-payments-subscriptions-empty-state-manager.php'; include_once __DIR__ . '/class-wc-payments-subscriptions-event-handler.php'; include_once __DIR__ . '/class-wc-payments-subscriptions-onboarding-handler.php'; include_once __DIR__ . '/class-wc-payments-subscription-minimum-amount-handler.php'; include_once __DIR__ . '/class-wc-payments-subscriptions-disabler.php'; // Instantiate additional classes. self::$product_service = new WC_Payments_Product_Service( $api_client, $account ); self::$invoice_service = new WC_Payments_Invoice_Service( $api_client, self::$order_service ); self::$subscription_service = new WC_Payments_Subscription_Service( $api_client, $customer_service, self::$product_service, self::$invoice_service ); self::$event_handler = new WC_Payments_Subscriptions_Event_Handler( self::$invoice_service, self::$subscription_service ); new WC_Payments_Subscription_Change_Payment_Method_Handler(); new WC_Payments_Subscriptions_Plugin_Notice_Manager(); new WC_Payments_Subscriptions_Empty_State_Manager( $account ); new WC_Payments_Subscriptions_Onboarding_Handler( $account ); new WC_Payments_Subscription_Minimum_Amount_Handler( $api_client ); // Only disable bundled subscriptions UI when the full WooCommerce Subscriptions plugin is not active. if ( ! class_exists( 'WC_Subscriptions' ) ) { ( new WC_Payments_Subscriptions_Disabler() )->init_hooks(); } if ( class_exists( 'WCS_Background_Repairer' ) && function_exists( 'wcs_get_orders_with_meta_query' ) ) { include_once __DIR__ . '/class-wc-payments-subscriptions-migrator.php'; self::$stripe_billing_migrator = new WC_Payments_Subscriptions_Migrator( $api_client, $token_service ); } } /** * Get the Event Handler class instance. * * @return WC_Payments_Subscriptions_Event_Handler */ public static function get_event_handler() { return self::$event_handler; } /** * Returns the the product service instance. * * @return WC_Payments_Product_Service The product service object. */ public static function get_product_service() { return self::$product_service; } /** * Returns the the invoice service instance. * * @return WC_Payments_Invoice_Service */ public static function get_invoice_service() { return self::$invoice_service; } /** * Returns the the subscription service instance. * * @return WC_Payments_Subscription_Service */ public static function get_subscription_service() { return self::$subscription_service; } /** * Returns the the Stripe Billing migrator instance. * * @return WC_Payments_Subscriptions_Migrator */ public static function get_stripe_billing_migrator() { return self::$stripe_billing_migrator; } /** * Determines if this is a duplicate/staging site. * * This function is a wrapper for WCS_Staging::is_duplicate_site(). * * @return bool Whether the site is a duplicate URL or not. */ public static function is_duplicate_site() { if ( class_exists( 'WC_Subscriptions' ) && version_compare( WC_Subscriptions::$version, '4.0.0', '<' ) && method_exists( 'WC_Subscriptions', 'is_duplicate_site' ) ) { return WC_Subscriptions::is_duplicate_site(); } return class_exists( 'WCS_Staging' ) && method_exists( 'WCS_Staging', 'is_duplicate_site' ) && WCS_Staging::is_duplicate_site(); } } includes/subscriptions/class-wc-payments-subscription-change-payment-method-handler.php 0000777 00000023351 15251156605 0025676 0 ustar 00 <?php /** * Class WC_Payments_Subscription_Change_Payment_Method * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; /** * Class handling any WCPay subscription change payment method functionality. */ class WC_Payments_Subscription_Change_Payment_Method_Handler { /** * Constructor. */ public function __construct() { if ( ! WC_Payments_Features::should_use_stripe_billing() ) { return; } // Add an "Update card" action to all WCPay billing subscriptions with a failed renewal order. add_filter( 'wcs_view_subscription_actions', [ $this, 'update_subscription_change_payment_button' ], 15, 2 ); add_filter( 'woocommerce_can_subscription_be_updated_to_new-payment-method', [ $this, 'can_update_payment_method' ], 15, 2 ); // Override the pay for order link on the order to redirect to a change payment method page. add_filter( 'woocommerce_my_account_my_orders_actions', [ $this, 'update_order_pay_button' ], 15, 2 ); // Filter elements/messaging on the "Change payment method" page to reflect updating a WCPay billing card. add_filter( 'woocommerce_subscriptions_change_payment_method_page_title', [ $this, 'change_payment_method_page_title' ], 10, 2 ); add_filter( 'woocommerce_subscriptions_change_payment_method_page_notice_message', [ $this, 'change_payment_method_page_notice' ], 10, 2 ); // Fallback to redirecting all pay for order pages for WCPay billing invoices to the update card page. add_action( 'template_redirect', [ $this, 'redirect_pay_for_order_to_update_payment_method' ] ); add_filter( 'woocommerce_change_payment_button_text', [ $this, 'change_payment_method_form_submit_text' ] ); } /** * Replaces the default change payment method action for WC Pay subscriptions when the subscription needs a new payment method after a failed attempt. * * @param array $actions The My Account > View Subscription actions. * @param WC_Subscription $subscription The subscription object. * * @return array The subscription actions. */ public function update_subscription_change_payment_button( $actions, $subscription ) { if ( $this->does_subscription_need_payment_updated( $subscription ) ) { // Override any existing button on $actions['change_payment_method'] to show "Update Card" button. $actions['change_payment_method'] = [ 'url' => $this->get_subscription_update_payment_url( $subscription ), 'name' => __( 'Update payment method', 'woocommerce-payments' ), ]; } return $actions; } /** * Updates the 'Pay' link displayed on the My Account > Orders or from a subscriptions related orders table, to make sure customers are directed to update their card. * * @param array $actions Order actions. * @param WC_Order $order The WC Order object. * * @return array The order actions. */ public function update_order_pay_button( $actions, $order ) { // If the order isn't payable, there's nothing to update. if ( ! isset( $actions['pay'] ) || ! function_exists( 'wcs_get_subscriptions_for_order' ) ) { return $actions; } // If there isn't an invoice linked to this order, there's nothing to update. if ( ! WC_Payments_Invoice_Service::get_order_invoice_id( $order ) ) { return $actions; } $subscriptions = wcs_get_subscriptions_for_order( $order, [ 'order_type' => 'any' ] ); $subscription = ! empty( $subscriptions ) ? array_pop( $subscriptions ) : null; // If we couldn't locate the subscription, we can assume this is a WCPay subscription by the fact the order has an invoice ID. // As a failsafe remove the 'pay' action for this order as that's not the accepted flow for WCPay subscriptions. if ( ! $subscription ) { unset( $actions['pay'] ); return $actions; } // Only alter the pay action if the subscription needs a new payment method. if ( $this->does_subscription_need_payment_updated( $subscription ) ) { $actions['pay']['url'] = $this->get_subscription_update_payment_url( $subscription ); } return $actions; } /** * Filters subscription `can_be_updated_to( 'new-payment-method' )` calls to allow customers to update their subscription's payment method. * * @param bool $can_update Whether the subscription's payment method can be updated. * @param WC_Subscription $subscription The WC Subscription object. * * @return bool Whether the subscription's payment method can be updated. */ public function can_update_payment_method( bool $can_update, WC_Subscription $subscription ) { return $this->does_subscription_need_payment_updated( $subscription ) ? true : $can_update; } /** * Redirects customers to update their payment method rather than pay for a WC Pay Subscription's failed order. */ public function redirect_pay_for_order_to_update_payment_method() { global $wp; // Note: There is no nonce verification for the "pay for order" action - the URL is long living. if ( ! isset( $_GET['pay_for_order'], $_GET['key'] ) || ! empty( $_GET['change_payment_method'] ) || ( ! isset( $_GET['order_id'] ) && ! isset( $wp->query_vars['order-pay'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } if ( ! function_exists( 'wcs_get_subscriptions_for_order' ) ) { return; } $order_id = ( isset( $wp->query_vars['order-pay'] ) ) ? absint( $wp->query_vars['order-pay'] ) : absint( $_GET['order_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $order_key = wc_clean( wp_unslash( $_GET['key'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $order = wc_get_order( $order_id ); if ( ! $order || ! hash_equals( $order->get_order_key(), $order_key ) || ! current_user_can( 'pay_for_order', $order->get_id() ) ) { return; } // Check if the order is linked to a billing invoice. $invoice_id = WC_Payments_Invoice_Service::get_order_invoice_id( $order ); if ( $invoice_id ) { $subscriptions = wcs_get_subscriptions_for_order( $order, [ 'order_type' => 'any' ] ); if ( ! empty( $subscriptions ) ) { $subscription = array_pop( $subscriptions ); if ( $subscription && current_user_can( 'edit_shop_subscription_payment_method', $subscription->get_id() ) && $this->does_subscription_need_payment_updated( $subscription ) ) { wp_safe_redirect( $this->get_subscription_update_payment_url( $subscription ) ); exit; } } } } /** * Modifies the change payment method page title (and page breadcrumbs) when updating card details for WC Pay subscriptions. * * @param string $title The default page title. * @param WC_Subscription $subscription The WC Subscription object. * * @return string The page title. */ public function change_payment_method_page_title( string $title, WC_Subscription $subscription ) { if ( $this->does_subscription_need_payment_updated( $subscription ) ) { $title = __( 'Update payment details', 'woocommerce-payments' ); } return $title; } /** * Modifies the message shown on the change payment method page. * * @param string $message The default customer notice shown on the change payment method page. * @param WC_Subscription $subscription The Subscription. * * @return string The customer notice shown on the change payment method page. */ public function change_payment_method_page_notice( string $message, WC_Subscription $subscription ) { if ( $this->does_subscription_need_payment_updated( $subscription ) ) { $message = __( "Your subscription's last renewal failed payment. Please update your payment details so we can reattempt payment.", 'woocommerce-payments' ); } return $message; } /** * Checks if a subscription needs to update it's WCPay payment method. * * @param WC_Subscription $subscription The WC Subscription object. * @return bool Whether the subscription's last order failed and needs a new updated payment method. */ private function does_subscription_need_payment_updated( $subscription ) { // We're only interested in WC Pay subscriptions that are on hold due to a failed payment. if ( ! is_a( $subscription, 'WC_Subscription' ) || ! $subscription->has_status( 'on-hold' ) || ! WC_Payments_Subscription_Service::is_wcpay_subscription( $subscription ) ) { return false; } $last_order = $subscription->get_last_order( 'all', 'any' ); return $last_order && $last_order->has_status( 'failed' ) && WC_Payments_Invoice_Service::get_pending_invoice_id( $subscription ); } /** * Generates the URL for the WC Pay Subscription's update payment method screen. * * @param WC_Subscription $subscription The WC Subscription object. * @return string The update payment method */ private function get_subscription_update_payment_url( $subscription ) { return add_query_arg( // nosemgrep: audit.php.wp.security.xss.query-arg -- no user input is used in this URL. [ 'change_payment_method' => $subscription->get_id(), '_wpnonce' => wp_create_nonce(), ], $subscription->get_checkout_payment_url() ); } /** * Modifies the change payment method form submit button to include language about retrying payment if there's a failed order. * * @param string $button_text The change subscription payment method button text. * @return string The change subscription payment method button text. */ public function change_payment_method_form_submit_text( $button_text ) { if ( isset( $_GET['change_payment_method'] ) && function_exists( 'wcs_get_subscription' ) ) { // phpcs:ignore WordPress.Security.NonceVerification $subscription = wcs_get_subscription( wc_clean( wp_unslash( $_GET['change_payment_method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification if ( $subscription && $this->does_subscription_need_payment_updated( $subscription ) ) { $button_text = __( 'Update and retry payment', 'woocommerce-payments' ); } } return $button_text; } } includes/subscriptions/class-wc-payments-subscriptions-disabler.php 0000777 00000056777 15251156605 0022077 0 ustar 00 <?php /** * Class WC_Payments_Subscriptions_Disabler * * Responsible for disabling merchant and customer facing management * interfaces for bundled subscriptions while keeping renewal logic active. * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Disables bundled subscriptions management surfaces. * * This class hides UI elements and blocks access to subscription management * interfaces for both merchants and customers. It also prevents new subscription * products from being purchased or added to orders. * * What this class disables: * - Admin menu items (WooCommerce > Subscriptions) * - Admin subscription management screens * - Subscription product types in product creation * - Subscription settings tab * - Customer account subscription pages * - Related subscriptions section on order details * - Related orders meta box on admin order edit screen * - Purchasing of subscription products (makes them unpurchasable) * - Adding subscription products to admin orders (both search and validation) * - Order-pay endpoint when accessed with subscription IDs * * What this class does NOT affect: * - Stripe Billing webhook processing (invoice.paid, invoice.upcoming, etc.) * - Automatic renewal order creation via wcs_create_renewal_order() * - Subscription payment processing and completion * - Existing subscription data or meta * - Backend subscription status management * - Payment method updates * - Regular (non-subscription) products * * This ensures merchants and customers cannot create or manage subscriptions * through the UI while Stripe Billing continues to process renewals automatically. */ class WC_Payments_Subscriptions_Disabler { /** * Initiates hooks that hide bundled subscriptions management entry points. * * This method registers UI-layer hooks only. It does NOT hook into: * - Payment processing (woocommerce_subscription_payment_complete, etc.) * - Renewal order creation (woocommerce_renewal_order_payment_complete, etc.) * - Webhook handling (invoice.paid, invoice.upcoming, etc.) * - Subscription status changes (woocommerce_subscription_status_*, etc.) * * Admin hooks (menu/screen blocking): * - Removes admin menu items * - Blocks direct access to subscription screens * - Removes subscription product types from product editor * - Removes subscription settings tab * - Removes "Related Orders" meta box from order edit screen * * Frontend hooks (customer-facing blocking): * - Removes subscription navigation from My Account * - Blocks direct access to subscription endpoints * - Removes subscription details from order views * - Makes subscription products unpurchasable (prevents new subscriptions) * * @return void */ public function init_hooks() { if ( is_admin() ) { add_action( 'admin_menu', [ $this, 'remove_admin_menu_items' ], 99 ); add_action( 'current_screen', [ $this, 'maybe_block_admin_subscription_screen' ] ); add_filter( 'product_type_selector', [ $this, 'filter_product_type_selector' ], 99 ); add_filter( 'woocommerce_settings_tabs_array', [ $this, 'filter_settings_tabs' ], 99 ); add_action( 'admin_init', [ $this, 'maybe_redirect_settings_tab' ], 99 ); add_action( 'admin_notices', [ $this, 'display_subscription_disabled_notice' ] ); add_filter( 'woocommerce_json_search_found_products', [ $this, 'filter_admin_product_search' ] ); add_filter( 'woocommerce_ajax_add_order_item_validation', [ $this, 'validate_admin_order_item' ], 10, 4 ); add_action( 'add_meta_boxes', [ $this, 'remove_related_orders_meta_box' ], 99, 2 ); } add_filter( 'woocommerce_account_menu_items', [ $this, 'remove_account_menu_item' ], 99 ); add_action( 'pre_get_posts', [ $this, 'maybe_redirect_subscription_endpoints' ], 1 ); add_action( 'template_redirect', [ $this, 'maybe_redirect_account_endpoints' ], 5 ); add_action( 'init', [ $this, 'remove_related_subscriptions_section' ], 99 ); add_filter( 'woocommerce_is_purchasable', [ $this, 'make_subscription_products_unpurchasable' ], 10, 2 ); add_filter( 'woocommerce_cart_item_removed_message', [ $this, 'filter_subscription_removal_message' ], 10, 2 ); } /** * Removes WooCommerce > Subscriptions menu entries. * * Hides the subscriptions admin menu for both CPT and HPOS implementations. * Does not affect subscription data or the ability for renewals to process. * * @return void */ public function remove_admin_menu_items() { remove_submenu_page( 'woocommerce', 'edit.php?post_type=shop_subscription' ); remove_submenu_page( 'woocommerce', 'wc-orders--shop_subscription' ); remove_menu_page( 'wc-orders--shop_subscription' ); } /** * Removes the "Related Orders" meta box from order edit screens. * * This meta box displays subscription-related orders (renewals, parent orders, etc.) * on the admin order edit screen. We remove it to hide subscription relationships * from merchants when viewing orders. * * The meta box is added by WooCommerce Subscriptions via: * - WCS_Admin_Meta_Boxes::add_meta_boxes() at priority 10 on 'add_meta_boxes' * - Meta box ID: 'subscription_renewal_orders' * - Title: 'Related Orders' * * We run this at priority 99 to ensure it executes after WCS adds the meta box. * * @param string $post_type The post type of the current post being edited. * @param WP_Post|WC_Order|null $post_or_order_object The post or order currently being edited. * @return void */ public function remove_related_orders_meta_box( $post_type, $post_or_order_object = null ) { // Only process when WCS functions are available. if ( ! function_exists( 'wcs_get_page_screen_id' ) ) { return; } // Get the order screen ID (handles both CPT and HPOS). $order_screen_id = wcs_get_page_screen_id( 'shop_order' ); // Remove the Related Orders meta box from the order edit screen. // The 'normal' context matches where WCS registers the meta box. remove_meta_box( 'subscription_renewal_orders', $order_screen_id, 'normal' ); } /** * Redirects attempts to access admin subscription management screens. * * Prevents direct URL access to subscription edit/list screens by redirecting * to the WooCommerce overview. Does not run during AJAX or REST requests to * avoid interfering with legitimate background operations. * * @param WP_Screen $screen Current screen instance. * @return void */ public function maybe_block_admin_subscription_screen( $screen ) { if ( wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { return; } if ( ! $screen instanceof WP_Screen ) { return; } $screen_id = (string) $screen->id; if ( $this->is_blocked_admin_screen( $screen_id ) || $this->is_subscription_post_type_request() ) { $this->redirect_to_admin_overview(); } } /** * Removes the subscriptions tab from the My Account navigation. * * @param array $items My Account menu items. * @return array Filtered menu items. */ public function remove_account_menu_item( $items ) { $subscriptions_endpoint = $this->get_account_endpoint_slug( 'subscriptions' ); if ( isset( $items[ $subscriptions_endpoint ] ) ) { unset( $items[ $subscriptions_endpoint ] ); } return $items; } /** * Removes subscription related product types from product selector. * * Prevents merchants from creating new subscription products by hiding * the product types from the dropdown. Existing subscription products * remain in the database and can still process renewals. * * @param array $product_types Registered product types. * @return array Filtered product types without subscription options. */ public function filter_product_type_selector( $product_types ) { unset( $product_types['subscription'], $product_types['variable-subscription'] ); return $product_types; } /** * Removes subscription tab from WooCommerce settings. * * @param array $tabs Registered WooCommerce settings tabs. * @return array */ public function filter_settings_tabs( $tabs ) { unset( $tabs['subscriptions'] ); return $tabs; } /** * Redirects attempts to access the removed subscriptions settings tab. * * @return void */ public function maybe_redirect_settings_tab() { if ( empty( $_GET['page'] ) || empty( $_GET['tab'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $page = sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $tab = sanitize_key( wp_unslash( $_GET['tab'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'wc-settings' !== $page || 'subscriptions' !== $tab ) { return; } $this->redirect( add_query_arg( [ 'page' => 'wc-settings', 'tab' => 'general', ], admin_url( 'admin.php' ) ) ); } /** * Redirects subscription endpoints during query parsing. * * This runs on the pre_get_posts hook (priority 1) to intercept subscription * endpoint requests BEFORE WooCommerce Subscriptions can redirect them to * the order-pay page. This is critical because WCS_Query::maybe_redirect_payment_methods() * runs at priority 10 on pre_get_posts and would redirect /my-account/subscription-payment-method/ID * to /checkout/order-pay/ID/?change_payment_method=ID before we can block it. * * @param WP_Query $query The WP_Query instance. * @return void */ public function maybe_redirect_subscription_endpoints( $query ) { // Only process main queries. if ( ! $query->is_main_query() ) { return; } // Check each subscription endpoint. $endpoints = [ 'subscriptions', 'view-subscription', 'subscription-payment-method', ]; foreach ( $endpoints as $endpoint_key ) { $endpoint_slug = $this->get_account_endpoint_slug( $endpoint_key ); // Check if this query is for a subscription endpoint. if ( ! empty( $query->get( $endpoint_slug ) ) ) { // Redirect to My Account before WCS can redirect elsewhere. $this->redirect( wc_get_page_permalink( 'myaccount' ) ); } } } /** * Redirects subscription related customer account endpoints. * * Prevents customers from accessing subscription management pages including: * - Subscriptions list (/my-account/subscriptions) * - View subscription detail (/my-account/view-subscription/123) * - Payment method management (/my-account/subscription-payment-method/123) * * Redirects all attempts to the My Account dashboard. Does not affect * subscription data or automated renewal payments. * * @return void */ public function maybe_redirect_account_endpoints() { foreach ( $this->get_blocked_account_endpoints() as $endpoint ) { if ( empty( $endpoint ) ) { continue; } if ( $this->is_endpoint_url( $endpoint ) ) { $this->redirect( wc_get_page_permalink( 'myaccount' ) ); } } // Also block order-pay endpoint if it contains a subscription ID. $this->maybe_redirect_order_pay_for_subscription(); } /** * Redirects order-pay requests that target subscription IDs. * * This prevents users from accessing the "pay for order" page using a * subscription ID in two ways: * * 1. Direct access: /checkout/order-pay/{subscription_id}/ * 2. Via change_payment_method parameter: /checkout/order-pay/ID/?change_payment_method={subscription_id} * * The second case occurs when WooCommerce Subscriptions redirects from * /my-account/subscription-payment-method/{subscription_id}/ to the order-pay * endpoint during the pre_get_posts hook. * * @return void */ private function maybe_redirect_order_pay_for_subscription() { global $wp; $subscription_id = null; // Check if we're on the order-pay endpoint with a subscription ID. if ( ! empty( $wp->query_vars['order-pay'] ) ) { $order_id = absint( $wp->query_vars['order-pay'] ); $post_type = get_post_type( $order_id ); if ( 'shop_subscription' === $post_type ) { $subscription_id = $order_id; } } // Also check for change_payment_method parameter (when redirected from subscription-payment-method endpoint). // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified by WooCommerce core. if ( ! $subscription_id && ! empty( $_GET['change_payment_method'] ) ) { $change_payment_id = absint( $_GET['change_payment_method'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $post_type = get_post_type( $change_payment_id ); if ( 'shop_subscription' === $post_type ) { $subscription_id = $change_payment_id; } } // If we found a subscription ID in either place, redirect. if ( $subscription_id ) { $this->redirect( wc_get_page_permalink( 'myaccount' ) ); } } /** * Removes the related subscriptions section from order details. * * Hides the "Related Subscriptions" section that normally appears on * order detail pages (both admin and customer-facing). This prevents * users from viewing subscription information through renewal orders. * * The underlying subscription and renewal order relationship remains intact; * only the display is hidden. * * @return void */ public function remove_related_subscriptions_section() { if ( class_exists( 'WC_Subscriptions_Order' ) ) { remove_action( 'woocommerce_order_details_after_order_table', [ 'WC_Subscriptions_Order', 'add_subscriptions_to_view_order_templates' ], 10 ); } } /** * Makes subscription products unpurchasable to prevent new subscriptions. * * This prevents customers from adding subscription products to their cart * or purchasing them during checkout. Runs early in the purchase flow to * provide the cleanest user experience. * * Does NOT affect: * - Renewal order processing (renewals don't check is_purchasable) * - Existing subscriptions in the database * - Regular (non-subscription) products * * @param bool $is_purchasable Whether the product can be purchased. * @param WC_Product $product Product object. * @return bool False for subscription products, original value otherwise. */ public function make_subscription_products_unpurchasable( $is_purchasable, $product ) { if ( ! $product ) { return $is_purchasable; } // Check if product is a subscription type. if ( $product->is_type( [ 'subscription', 'variable-subscription', 'subscription_variation' ] ) ) { return false; } return $is_purchasable; } /** * Filters the cart item removal message for subscription products. * * When WooCommerce removes unpurchasable products from the cart, this filter * customizes the message for subscription products to be more customer-friendly. * * @param string $message The default removal message from WooCommerce. * @param WC_Product $product The product being removed. * @return string The filtered message. */ public function filter_subscription_removal_message( $message, $product ) { // Only modify the message if this is a subscription product. if ( ! $product || ! $product->is_type( [ 'subscription', 'variable-subscription', 'subscription_variation' ] ) ) { return $message; } // Return a customer-friendly message that matches WooCommerce's standard format. return sprintf( /* translators: %s: product name */ __( '%s has been removed from your cart because it can no longer be purchased. Please contact us if you need assistance.', 'woocommerce-payments' ), $product->get_name() ); } /** * Filters subscription products from admin product search results. * * Removes subscription products from the AJAX product search used in the * admin order editor "Add item(s)" modal. This prevents admins from seeing * subscription products as options when manually creating orders. * * @param array $products Array of products (product_id => product_name). * @return array Filtered array without subscription products. */ public function filter_admin_product_search( $products ) { if ( empty( $products ) ) { return $products; } $filtered = []; foreach ( $products as $product_id => $product_name ) { $product = wc_get_product( $product_id ); // Skip if not a valid product or is a subscription type. if ( ! $product || $product->is_type( [ 'subscription', 'variable-subscription', 'subscription_variation' ] ) ) { continue; } $filtered[ $product_id ] = $product_name; } return $filtered; } /** * Validates that subscription products cannot be added to admin orders. * * This provides server-side validation as a backup to the search filtering. * If an admin somehow attempts to add a subscription product to an order * (e.g., by manipulating the AJAX request), this will block it with an error. * * @param WP_Error $validation_error Error object to populate if validation fails. * @param WC_Product $product Product being added to order. * @param WC_Order $order Order object. * @param int $qty Quantity being added. * @return WP_Error Error object (populated if validation fails). */ public function validate_admin_order_item( $validation_error, $product, $order, $qty ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Required by filter signature. unset( $order, $qty ); if ( ! $product ) { return $validation_error; } // Check if product is a subscription type. if ( $product->is_type( [ 'subscription', 'variable-subscription', 'subscription_variation' ] ) ) { return new WP_Error( 'subscription_not_allowed_in_admin_order', __( 'Subscription products cannot be added to orders. Please install WooCommerce Subscriptions to manage subscriptions.', 'woocommerce-payments' ) ); } return $validation_error; } /** * Determines if the given screen ID should be blocked. * * Checks if a screen ID contains subscription-related identifiers for * both CPT (shop_subscription) and HPOS (wc-orders--shop_subscription). * * @param string $screen_id Screen ID. * @return bool True if the screen should be blocked, false otherwise. */ private function is_blocked_admin_screen( $screen_id ) { if ( '' === $screen_id ) { return false; } return false !== strpos( $screen_id, 'shop_subscription' ) || false !== strpos( $screen_id, 'wc-orders--shop_subscription' ); } /** * Determines if the current request is targeting the subscription post type. * * This handles: * - Listing: ?post_type=shop_subscription * - Adding new: ?post_type=shop_subscription * - Editing by post ID: ?post=123&action=edit (checked via post type lookup) * * @return bool */ private function is_subscription_post_type_request() { // Check for explicit post_type parameter. if ( ! empty( $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return 'shop_subscription' === sanitize_key( wp_unslash( $_GET['post_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } // Check if editing a specific post that might be a subscription. if ( ! empty( $_GET['post'] ) && ! empty( $_GET['action'] ) && 'edit' === $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $post_id = absint( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $post_id > 0 ) { $post_type = get_post_type( $post_id ); // Block subscription orders. if ( 'shop_subscription' === $post_type ) { return true; } // Block subscription products. if ( 'product' === $post_type && $this->is_subscription_product( $post_id ) ) { return true; } } } return false; } /** * Checks if a product ID is a subscription product. * * @param int $product_id Product ID to check. * @return bool True if the product is a subscription product, false otherwise. */ private function is_subscription_product( $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product ) { return false; } // Check product type directly - more reliable than using WC_Subscriptions_Product // which may not be available in all contexts. return $product->is_type( [ 'subscription', 'variable-subscription', 'subscription_variation' ] ); } /** * Displays an admin notice when users are redirected from disabled subscription features. * * @return void */ public function display_subscription_disabled_notice() { if ( empty( $_GET['wcpay_subscription_disabled'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } if ( empty( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } if ( empty( $_GET['section'] ) || 'woocommerce_payments' !== $_GET['section'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $message = sprintf( /* translators: %1$s: WooCommerce Subscriptions link */ __( 'To access your subscriptions data and keep managing recurring payments, please install <a target="_blank" href="%1$s">WooCommerce Subscriptions</a>. Built-in support for subscriptions is no longer available in WooPayments.', 'woocommerce-payments' ), 'https://woocommerce.com/products/woocommerce-subscriptions/' ); ?> <div class="notice notice-info wcpay-notice"> <p><strong><?php esc_html_e( 'WooPayments', 'woocommerce-payments' ); ?></strong></p> <p> <?php echo wp_kses( $message, [ 'a' => [ 'href' => [], 'target' => [], ], ] ); ?> </p> </div> <?php } /** * Redirects the current request to the WooCommerce Payments settings page. * * Adds a query parameter to trigger an informational notice after redirect. * * @return void */ protected function redirect_to_admin_overview() { $redirect_url = add_query_arg( [ 'page' => 'wc-settings', 'tab' => 'checkout', 'section' => 'woocommerce_payments', 'wcpay_subscription_disabled' => '1', ], admin_url( 'admin.php' ) ); $this->redirect( $redirect_url ); } /** * Gets the account endpoint slug for the supplied option key. * * @param string $key Subscriptions endpoint option key suffix. * @return string */ private function get_account_endpoint_slug( $key ) { switch ( $key ) { case 'view-subscription': return get_option( 'woocommerce_myaccount_view_subscription_endpoint', 'view-subscription' ); case 'subscription-payment-method': return get_option( 'woocommerce_myaccount_subscription_payment_method_endpoint', 'subscription-payment-method' ); case 'subscriptions': default: return get_option( 'woocommerce_myaccount_subscriptions_endpoint', 'subscriptions' ); } } /** * Returns the list of account endpoints which should be blocked. * * Retrieves all subscription-related My Account endpoints that customers * should not be able to access. Endpoint slugs are configurable via * WooCommerce settings, so we fetch them dynamically. * * @return array Array of endpoint slugs to block. */ private function get_blocked_account_endpoints() { return [ $this->get_account_endpoint_slug( 'subscriptions' ), $this->get_account_endpoint_slug( 'view-subscription' ), $this->get_account_endpoint_slug( 'subscription-payment-method' ), ]; } /** * Redirects the current request to the provided URL and exits execution. * * @param string $target Target URL. * @return void */ protected function redirect( $target ) { wp_safe_redirect( $target ); exit; } /** * Checks whether the current request matches the provided endpoint. * * @param string $endpoint Endpoint slug. * @return bool */ protected function is_endpoint_url( $endpoint ) { return is_wc_endpoint_url( $endpoint ); } } includes/subscriptions/templates/html-woo-payments-deactivate-warning.php 0000777 00000005572 15251156605 0023174 0 ustar 00 <?php /** * Admin WC Subscriptions plugin warning template. * * @package WooCommerce\Payments */ ?> <script type="text/template" id="tmpl-wcpay-plugin-deactivate-warning"> <div id="wcpay-plugin-deactivate-warning-notice" class="wc-backbone-modal woopayments-plugin-warning-modal"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <h1><?php esc_html_e( 'Are you sure?', 'woocommerce-payments' ); ?></h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text">Close modal panel</span> </button> </header> <article> <p> <?php printf( // Translators: placeholders are opening and closing strong HTML tags. %6$s: WooPayments, %7$s: Woo Subscriptions. esc_html__( 'Your store has subscriptions using %6$s Stripe Billing functionality for payment processing. Due to the %1$soff-site billing engine%3$s these subscriptions use,%4$s they will continue to renew even after you deactivate %6$s%5$s.', 'woocommerce-payments' ), '<a href="https://woocommerce.com/document/woopayments/subscriptions/stripe-billing/#billing-engines" target="_blank">', '<a href="https://woocommerce.com/document/woopayments/subscriptions/stripe-billing/#deactivate-woopayments-plugin" target="_blank">', '</a>', '<strong>', '</strong>', 'WooPayments', ); ?> </br> </br> <?php printf( // translators: $1 $2 placeholders are opening and closing HTML link tags, linking to documentation. $3 is WooPayments. esc_html__( 'If you do not want these subscriptions to continue to be billed, you should %1$scancel these subscriptions%2$s prior to deactivating %3$s.', 'woocommerce-payments' ), '<a href="https://woocommerce.com/document/subscriptions/store-manager-guide/#cancel-or-suspend-subscription" target="_blank">', '</a>', 'WooPayments' ); ?> </p> <strong> <?php printf( // translators: Placeholder is "Woo Subscriptions"". esc_html__( 'Are you sure you want to deactivate %s?', 'woocommerce-payments' ), 'WooPayments' ); ?> </strong> </article> <footer> <div class="inner"> <button class="modal-close button button-secondary button-large"><?php esc_html_e( 'Cancel', 'woocommerce-payments' ); ?></button> <button id="wcpay-plugin-deactivate-modal-submit" class="button button-primary button-large"> <?php printf( // translators: Placeholder is "Woo Subscriptions"". esc_html__( 'Yes, deactivate %s', 'woocommerce-payments' ), 'WooPayments' ); ?> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> includes/subscriptions/templates/html-wcpay-deactivate-warning.php 0000777 00000005572 15251156605 0021655 0 ustar 00 <?php /** * Admin WooPayments plugin warning template. * * @package WooCommerce\Payments */ ?> <script type="text/template" id="tmpl-wcpay-plugin-deactivate-warning"> <div id="wcpay-plugin-deactivate-warning-notice" class="wc-backbone-modal woopayments-plugin-warning-modal"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <h1><?php esc_html_e( 'Are you sure?', 'woocommerce-payments' ); ?></h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text">Close modal panel</span> </button> </header> <article> <p> <?php printf( // translators: $1 $2 $3 placeholders are opening and closing HTML link tags, linking to documentation. $4 $5 placeholders are opening and closing strong HTML tags. $6 is WooPayments. esc_html__( 'Your store has active subscriptions using the built-in %6$s functionality. Due to the %1$soff-site billing engine%3$s these subscriptions use, %4$sthey will continue to renew even after you deactivate %6$s%5$s. %2$sLearn more%3$s.', 'woocommerce-payments' ), '<a href="https://woocommerce.com/document/woopayments/subscriptions/stripe-billing/" target="_blank">', '<a href="https://woocommerce.com/document/woopayments/subscriptions/stripe-billing/#deactivate-woopayments-plugin" target="_blank">', '</a>', '<strong>', '</strong>', 'WooPayments' ); ?> <p> </p> <?php printf( // translators: $1 $2 placeholders are opening and closing HTML link tags, linking to documentation. $3 is WooPayments. esc_html__( 'If you do not want these subscriptions to continue to be billed, you should %1$scancel all subscriptions%2$s prior to deactivating %3$s. ', 'woocommerce-payments' ), '<a href="https://woocommerce.com/document/subscriptions/store-manager-guide/#cancel-or-suspend-subscription" target="_blank">', '</a>', 'WooPayments' ); ?> </p> <strong> <?php printf( /* translators: %s: WooPayments. */ esc_html__( 'Are you sure you want to deactivate %s?', 'woocommerce-payments' ), 'WooPayments' ); ?> </strong> </article> <footer> <div class="inner"> <button class="modal-close button button-secondary button-large"><?php esc_html_e( 'Cancel', 'woocommerce-payments' ); ?></button> <button id="wcpay-plugin-deactivate-modal-submit" class="button button-primary button-large"> <?php /* translators: %s: WooPayments */ printf( esc_html__( 'Yes, deactivate %s', 'woocommerce-payments' ), 'WooPayments' ); ?> </button> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> includes/subscriptions/templates/html-subscriptions-plugin-notice.php 0000777 00000005477 15251156605 0022446 0 ustar 00 <?php /** * Admin WC Subscriptions plugin warning template. * * @package WooCommerce\Payments */ ?> <script type="text/template" id="tmpl-wcpay-subscriptions-plugin-warning"> <div id="wcpay-subscriptions-plugin-warning-notice" class="wc-backbone-modal woopayments-plugin-warning-modal"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <h1><?php esc_html_e( 'Are you sure?', 'woocommerce-payments' ); ?></h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text">Close modal panel</span> </button> </header> <article> <p> <?php printf( // Translators: %1-%4 placeholders are opening and closing a or strong HTML tags. %5$s: WooPayments, %6$s: Woo Subscriptions. esc_html__( 'Your store has subscriptions using %5$s Stripe Billing functionality for payment processing. Due to the %1$soff-site billing engine%2$s these subscriptions use,%3$s they will continue to renew even after you deactivate %6$s%4$s.', 'woocommerce-payments' ), '<a href="https://woocommerce.com/document/woopayments/subscriptions/stripe-billing/#faq" target="_blank">', '</a>', '<strong>', '</strong>', 'WooPayments', 'Woo Subscriptions' ); ?> </br> </br> <?php printf( // translators: $1 $2 placeholders are opening and closing HTML link tags, linking to documentation. $3 is WooPayments. esc_html__( 'If you do not want these subscriptions to continue to be billed, you should %1$scancel these subscriptions%2$s prior to deactivating %3$s.', 'woocommerce-payments' ), '<a href="https://woocommerce.com/document/subscriptions/store-manager-guide/#cancel-or-suspend-subscription" target="_blank">', '</a>', 'Woo Subscriptions' ); ?> </p> <strong> <?php printf( // translators: Placeholder is "Woo Subscriptions"". esc_html__( 'Are you sure you want to deactivate %s?', 'woocommerce-payments' ), 'Woo Subscriptions' ); ?> </strong> </article> <footer> <div class="inner"> <button class="modal-close button button-secondary button-large"><?php esc_html_e( 'Cancel', 'woocommerce-payments' ); ?></button> <button id="wcpay-subscriptions-plugin-deactivation-submit" class="button button-primary button-large"> <?php printf( // translators: Placeholder is "Woo Subscriptions"". esc_html__( 'Yes, deactivate %s', 'woocommerce-payments' ), 'Woo Subscriptions' ); ?> </button> </div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> includes/subscriptions/class-wc-payments-subscription-minimum-amount-handler.php 0000777 00000004741 15251156605 0024476 0 ustar 00 <?php /** * Class WC_Payments_Subscription_Minimum_Amount_Handler * * @package WooCommerce\Payments */ /** * The WC_Payments_Subscription_Minimum_Amount_Handler class */ class WC_Payments_Subscription_Minimum_Amount_Handler { use WC_Payments_Subscriptions_Utilities; /** * The API client object. * * @var WC_Payments_API_Client */ private $api_client; /** * The transient key used to store the minimum amounts for a given currency. * * @const string */ const MINIMUM_RECURRING_AMOUNT_TRANSIENT_KEY = 'wcpay_subscription_minimum_recurring_amounts'; /** * The length of time in seconds the minimum amount is stored in a transient. * * @const int */ const MINIMUM_RECURRING_AMOUNTS_TRANSIENT_EXPIRATION = DAY_IN_SECONDS; /** * Initialize the class. * * @param WC_Payments_API_Client $api_client The API client object. */ public function __construct( WC_Payments_API_Client $api_client ) { $this->api_client = $api_client; if ( WC_Payments_Features::should_use_stripe_billing() ) { add_filter( 'woocommerce_subscriptions_minimum_processable_recurring_amount', [ $this, 'get_minimum_recurring_amount' ], 10, 2 ); } } /** * Gets the minimum WC Pay Subscription recurring amount that can be transacted in a given currency. * * @param int|bool $minimum_amount The minimum amount that can be processed in recurring transactions. Can be an int (the minimum amount) or false if no minimum exists. * @param string $currency_code The currency to fetch the minimum amount in. * * @return float The minimum recurring amount. */ public function get_minimum_recurring_amount( $minimum_amount, $currency_code ) { $transient_key = self::MINIMUM_RECURRING_AMOUNT_TRANSIENT_KEY . "_$currency_code"; // Enforce uppercase. $transient_key = strtoupper( $transient_key ); // Minimum amount is purposefully immediately overwritten. The calling function passes a default value which we must receive. $minimum_amount = get_transient( $transient_key ); if ( false === $minimum_amount ) { try { $minimum_amount = $this->api_client->get_currency_minimum_recurring_amount( $currency_code ); } catch ( \WCPay\Exceptions\API_Exception $exception ) { // Currency not supported or other API error. $minimum_amount = 0; } set_transient( $transient_key, $minimum_amount, self::MINIMUM_RECURRING_AMOUNTS_TRANSIENT_EXPIRATION ); } return WC_Payments_Utils::interpret_stripe_amount( (int) $minimum_amount, strtolower( $currency_code ) ); } } includes/subscriptions/class-wc-payments-subscriptions-migrator.php 0000777 00000100257 15251156605 0022115 0 ustar 00 <?php /** * Class WC_Payments_Subscriptions_Migrator * * @package WooCommerce\Payments */ use WCPay\Exceptions\API_Exception; require_once __DIR__ . '/class-wc-payments-subscription-migration-log-handler.php'; /** * Handles migrating WCPay Subscriptions to tokenized subscriptions. * * This class extends the WCS_Background_Repairer for scheduling and running the individual migration actions. */ class WC_Payments_Subscriptions_Migrator extends WCS_Background_Repairer { /** * Valid subscription statuses to cancel a subscription at Stripe. * * @var array $active_statuses */ private $active_statuses = [ 'active', 'past_due', 'trialing', 'paused' ]; /** * WCPay Subscription meta keys for migrated data. * * @var array $migrated_meta_keys */ private $meta_keys_to_migrate = [ WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, WC_Payments_Invoice_Service::ORDER_INVOICE_ID_KEY, WC_Payments_Invoice_Service::PENDING_INVOICE_ID_KEY, WC_Payments_Subscription_Service::SUBSCRIPTION_DISCOUNT_IDS_META_KEY, ]; /** * WC_Payments_API_Client instance. * * @var WC_Payments_API_Client */ private $api_client; /** * WC_Payments_Token_Service instance. * * @var WC_Payments_Token_Service */ private $token_service; /** * WC_Payments_Subscription_Migration_Log_Handler instance. * * @var WC_Payments_Subscription_Migration_Log_Handler */ protected $logger; /** * The Action Scheduler hook used to find and schedule individual migrations of WCPay Subscriptions. * * @var string */ public $scheduled_hook = 'wcpay_schedule_subscription_migrations'; /** * The Action Scheduler hook to migrate a WCPay Subscription. * * @var string */ public $migrate_hook = 'wcpay_migrate_subscription'; /** * The option name used to store a batch identifier for the current migration batch. * * @var string */ private $migration_batch_identifier_option = 'wcpay_subscription_migration_batch'; /** * Constructor. * * @param WC_Payments_API_Client|null $api_client WC_Payments_API_Client instance. * @param WC_Payments_Token_Service|null $token_service WC_Payments_Token_Service instance. */ public function __construct( $api_client = null, $token_service = null ) { $this->api_client = $api_client; $this->token_service = $token_service; $this->logger = new WC_Payments_Subscription_Migration_Log_Handler(); // Don't copy migrated subscription meta keys to related orders. add_filter( 'wc_subscriptions_object_data', [ $this, 'exclude_migrated_meta' ], 10, 1 ); // Add manual migration tool to WooCommerce > Status > Tools. add_filter( 'woocommerce_debug_tools', [ $this, 'add_manual_migration_tool' ] ); // Schedule the single migration action with two args. This is needed because the WCS_Background_Repairer parent class only hooks on with one arg. add_action( $this->migrate_hook . '_retry', [ $this, 'migrate_wcpay_subscription' ], 10, 2 ); $this->init(); } /** * Migrates a WCPay Subscription to a tokenized WooPayments subscription powered by WC Subscriptions * * Migration process: * 1. Validate the request to migrate subscription * 2. Fetches the subscription from Stripe * 3. Cancels the subscription at Stripe if it is active * 4. Update the subscription meta to indicate that it has been migrated * 5. Add an order note on the subscription * * @param int $subscription_id The ID of the subscription to migrate. * @param int $attempt The number of times migration has been attempted. */ public function migrate_wcpay_subscription( $subscription_id, $attempt = 0 ) { try { add_action( 'action_scheduler_unexpected_shutdown', [ $this, 'handle_unexpected_shutdown' ], 10, 2 ); add_action( 'action_scheduler_failed_execution', [ $this, 'handle_unexpected_action_failure' ], 10, 2 ); $this->logger->log( sprintf( 'Migrating subscription #%1$d.%2$s', $subscription_id, ( $attempt > 0 ? ' Attempt: ' . ( (int) $attempt + 1 ) : '' ) ) ); $subscription = $this->validate_subscription_to_migrate( $subscription_id ); $wcpay_subscription = $this->fetch_wcpay_subscription( $subscription ); $this->maybe_cancel_wcpay_subscription( $wcpay_subscription ); if ( $subscription->has_status( 'active' ) ) { $this->update_next_payment_date( $subscription, $wcpay_subscription ); } // If the subscription is active or on-hold, verify the payment method is valid and set correctly that it continues to renew. if ( $subscription->has_status( [ 'active', 'on-hold' ] ) ) { $this->verify_subscription_payment_token( $subscription, $wcpay_subscription ); } $this->update_wcpay_subscription_meta( $subscription ); if ( WC_Payment_Gateway_WCPay::GATEWAY_ID === $subscription->get_payment_method() ) { $subscription->add_order_note( __( 'This subscription has been successfully migrated to a WooPayments tokenized subscription.', 'woocommerce-payments' ) ); } $this->logger->log( sprintf( '---- Subscription #%d migration complete.', $subscription_id ) ); } catch ( \Exception $e ) { $this->logger->log( $e->getMessage() ); $this->maybe_reschedule_migration( $subscription_id, $attempt, $e ); } remove_action( 'action_scheduler_unexpected_shutdown', [ $this, 'handle_unexpected_shutdown' ] ); remove_action( 'action_scheduler_failed_execution', [ $this, 'handle_unexpected_action_failure' ] ); } /** * Validates the request to migrate a WCPay Subscription. * * Only allows migration if: * - The WooCommerce Subscription extension is active * - Store is not in staging mode or is a duplicate site * - The subscription ID is a valid subscription * - The subscription has not already been migrated * * @param int $subscription_id The ID of the subscription to migrate. * * @throws \Exception Skip the migration if the request is invalid. */ private function validate_subscription_to_migrate( $subscription_id ) { if ( ! class_exists( 'WC_Subscriptions' ) ) { throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. The WooCommerce Subscriptions extension is not active.', $subscription_id ) ); } if ( WC_Payments_Subscriptions::is_duplicate_site() ) { throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. Site is in staging mode.', $subscription_id ) ); } $subscription = wcs_get_subscription( $subscription_id ); if ( ! $subscription ) { throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. Subscription not found.', $subscription_id ) ); } $migrated_wcpay_subscription_id = $subscription->get_meta( '_migrated_wcpay_subscription_id', true ); if ( ! empty( $migrated_wcpay_subscription_id ) ) { throw new \Exception( sprintf( '---- Skipping migration of subscription #%1$d (%2$s). Subscription has already been migrated.', $subscription_id, $migrated_wcpay_subscription_id ) ); } return $subscription; } /** * Fetches the subscription from Stripe and verifies it has a valid ID and status. * * Returns false if the request returns an unexpected result. * * @param WC_Subscription $subscription The WC subscription to migrate. * * @return array * * @throws \Exception If there's an error fetching the subscription from Stripe. */ private function fetch_wcpay_subscription( $subscription ) { $wcpay_subscription_id = WC_Payments_Subscription_Service::get_wcpay_subscription_id( $subscription ); if ( ! $wcpay_subscription_id ) { throw new \Exception( sprintf( '---- Skipping migration of subscription #%d. Subscription is not a WCPay Subscription.', $subscription->get_id() ) ); } try { // Fetch the subscription from Stripe. $wcpay_subscription = $this->api_client->get_subscription( $wcpay_subscription_id ); } catch ( API_Exception $e ) { throw new \Exception( sprintf( '---- ERROR: Failed to fetch subscription #%1$d (%2$s) from Stripe. %3$s', $subscription->get_id(), $wcpay_subscription_id, $e->getMessage() ) ); } if ( empty( $wcpay_subscription['id'] ) || empty( $wcpay_subscription['status'] ) ) { throw new \Exception( sprintf( '---- ERROR: Cannot migrate subscription #%1$d (%2$s). Invalid data fetched from Stripe: %3$s', $subscription->get_id(), $wcpay_subscription_id, var_export( $wcpay_subscription, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export } return $wcpay_subscription; } /** * Cancels the subscription at Stripe if it is active. * * This function checks the status on the subscription at Stripe then cancels it if it's a valid status and logs any errors. * * We skip canceling any subscriptions at Stripe that are: * - incomplete: the subscription was created but no payment method was added to the subscription * - incomplete_expired: the incomplete subscription expired after 24hrs of no payment method being added. * - canceled: the subscription is already canceled * - unpaid: this status is not used by subscriptions in WooCommerce Payments * * @param array $wcpay_subscription The subscription data from Stripe. * * @throws \Exception If there's an error canceling the subscription at Stripe. */ private function maybe_cancel_wcpay_subscription( $wcpay_subscription ) { // Valid statuses to cancel subscription at Stripe: active, past_due, trialing, paused. if ( in_array( $wcpay_subscription['status'], $this->active_statuses, true ) ) { $this->logger->log( sprintf( '---- Stripe subscription (%1$s) has "%2$s" status. Canceling the subscription.', $wcpay_subscription['id'], $this->get_wcpay_subscription_status( $wcpay_subscription ) ) ); try { // Cancel the subscription in Stripe. $wcpay_subscription = $this->api_client->cancel_subscription( $wcpay_subscription['id'] ); } catch ( API_Exception $e ) { throw new \Exception( sprintf( '---- ERROR: Failed to cancel the Stripe subscription (%1$s). %2$s', $wcpay_subscription['id'], $e->getMessage() ) ); } $this->logger->log( sprintf( '---- Stripe subscription (%1$s) successfully canceled.', $wcpay_subscription['id'] ) ); } else { // Statuses that don't need to be canceled: incomplete, incomplete_expired, canceled, unpaid. $this->logger->log( sprintf( '---- Stripe subscription (%1$s) has "%2$s" status. Skipping canceling the subscription at Stripe.', $wcpay_subscription['id'], $this->get_wcpay_subscription_status( $wcpay_subscription ) ) ); } } /** * Migrates WCPay Subscription related metadata to a new key prefixed with `_migrated` and deletes the old meta. * * @param WC_Subscription $subscription The subscription with wcpay meta saved. */ private function update_wcpay_subscription_meta( $subscription ) { $updated = false; /** * If this subscription is being migrated while scheduling individual actions is on-going, make sure we store meta on the subscription * so that it's still returned by the query in @see get_items_to_repair() to not affect the limit and pagination. */ $migration_start = get_option( $this->migration_batch_identifier_option, 0 ); if ( 0 !== $migration_start ) { $subscription->update_meta_data( '_wcpay_subscription_migrated_during', $migration_start ); $updated = true; } foreach ( $this->meta_keys_to_migrate as $meta_key ) { if ( $subscription->meta_exists( $meta_key ) ) { $subscription->update_meta_data( '_migrated' . $meta_key, $subscription->get_meta( $meta_key, true ) ); $subscription->delete_meta_data( $meta_key ); $updated = true; } } if ( $updated ) { $subscription->save(); } } /** * Updates the subscription's next payment date in WooCommerce to ensure a smooth transition to on-site billing. * * There's a scenario where a WCPay subscription is active but has no pending renewal scheduled action. * Once migrated, this results in an active subscription that will remain active forever, without processing a renewal order. * * To ensure that all migrated subscriptions have a pending scheduled action, we need to reschedule the next payment date by * updating the date on the subscription. * * In priority order the new next payment date will be: * - The existing WooCommerce next payment date if it's in the future. * - The Stripe subscription's current_period_end if it's in the future. * - A newly calculated next payment date using the WC_Subscription::calculate_date() method. * * @param WC_Subscription $subscription The WC Subscription being migrated. * @param array $wcpay_subscription The subscription data from Stripe. */ private function update_next_payment_date( $subscription, $wcpay_subscription ) { try { // Just update the existing WC Subscription's next payment date if it's in the future. if ( $subscription->get_time( 'next_payment' ) > time() ) { $new_next_payment = gmdate( 'Y-m-d H:i:s', $subscription->get_time( 'next_payment' ) + 1 ); $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); $this->logger->log( sprintf( '---- Next payment date updated to %1$s to ensure subscription #%2$d has a pending scheduled payment.', $new_next_payment, $subscription->get_id() ) ); return; } // If the subscription was still using WooPayments, use the Stripe subscription's next payment time (current_period_end) if it's in the future. if ( WC_Payment_Gateway_WCPay::GATEWAY_ID === $subscription->get_payment_method() && isset( $wcpay_subscription['current_period_end'] ) && absint( $wcpay_subscription['current_period_end'] ) > time() ) { $new_next_payment = gmdate( 'Y-m-d H:i:s', absint( $wcpay_subscription['current_period_end'] ) ); $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); $this->logger->log( sprintf( '---- Next payment date updated to %1$s to match Stripe subscription record and to ensure subscription #%2$d has a pending scheduled payment.', $new_next_payment, $subscription->get_id() ) ); return; } // Lastly calculate the next payment date. $new_next_payment = $subscription->calculate_date( 'next_payment' ); if ( wcs_date_to_time( $new_next_payment ) > time() ) { $subscription->update_dates( [ 'next_payment' => $new_next_payment ] ); $this->logger->log( sprintf( '---- Calculated a new next payment date (%1$s) to ensure subscription #%2$d has a pending scheduled payment in the future.', $new_next_payment, $subscription->get_id() ) ); return; } // If we got here the next payment date is in the past, the Stripe subscription is missing a "current_period_end" or it's in the past, and calculating a new date also failed. Log an error. $this->logger->log( sprintf( '---- ERROR: Failed to update subscription #%1$d next payment date. Current next payment date (%2$s) is in the past, Stripe "current_period_end" data is invalid (%3$s) and an attempt to calculate a new date also failed (%4$s).', $subscription->get_id(), gmdate( 'Y-m-d H:i:s', $subscription->get_time( 'next_payment' ) ), isset( $wcpay_subscription['current_period_end'] ) ? gmdate( 'Y-m-d H:i:s', absint( $wcpay_subscription['current_period_end'] ) ) : 'no data', $new_next_payment ) ); } catch ( \Exception $e ) { $this->logger->log( sprintf( '---- ERROR: Failed to update subscription #%1$d next payment date. %2$s', $subscription->get_id(), $e->getMessage() ) ); } } /** * Returns the subscription status from the WCPay subscription data for logging purposes. * * If a subscription is on-hold in WC we wouldn't have changed the status of the subscription at Stripe, instead, the * subscription would remain active and set `pause_collection` behavior to `void` so that the subscription is not charged. * * The purpose of this function is to handle the `paused_collection` value when mapping the subscription status at Stripe to * a status for logging. * * @param array $wcpay_subscription The subscription data from Stripe. * * @return string The WCPay subscription status for logging purposes. */ private function get_wcpay_subscription_status( $wcpay_subscription ) { if ( empty( $wcpay_subscription['status'] ) ) { return 'unknown'; } if ( 'active' === $wcpay_subscription['status'] && ! empty( $wcpay_subscription['pause_collection']['behavior'] ) && 'void' === $wcpay_subscription['pause_collection']['behavior'] ) { return 'paused'; } return $wcpay_subscription['status']; } /** * Verifies the payment token on the subscription matches the default payment method on the WCPay Subscription. * * This function does two things: * 1. If the subscription doesn't have a WooPayments payment token, set it to the default payment method from Stripe Billing. * 2. If the subscription has a token, verify the token matches the token on the Stripe Billing subscription * * @param WC_Subscription $subscription The subscription to verify the payment token on. * @param array $wcpay_subscription The subscription data from Stripe. */ private function verify_subscription_payment_token( $subscription, $wcpay_subscription ) { // If the subscription's payment method isn't set to WooPayments, we skip this token step. if ( $subscription->get_payment_method() !== WC_Payment_Gateway_WCPay::GATEWAY_ID ) { $this->logger->log( sprintf( '---- Skipped verifying the payment token. Subscription #%1$d has "%2$s" as the payment method.', $subscription->get_id(), $subscription->get_payment_method() ) ); return; } if ( empty( $wcpay_subscription['default_payment_method'] ) ) { $this->logger->log( sprintf( '---- Could not verify the payment method. Stripe Billing subscription (%1$s) does not have a default payment method.', $wcpay_subscription['id'] ?? 'unknown' ) ); return; } $tokens = $subscription->get_payment_tokens(); $token_id = end( $tokens ); $token = ! $token_id ? null : WC_Payment_Tokens::get( $token_id ); // If the token matches the default payment method on the Stripe Billing subscription, we're done here. if ( $token && $token->get_token() === $wcpay_subscription['default_payment_method'] ) { $this->logger->log( sprintf( '---- Payment token on subscription #%1$d matches the payment method on the Stripe Billing subscription (%2$s).', $subscription->get_id(), $wcpay_subscription['id'] ?? 'unknown' ) ); return; } // At this point we know the subscription doesn't have a token or the token doesn't match, add one using the default payment method on the WCPay Subscription. $new_token = $this->maybe_create_and_update_payment_token( $subscription, $wcpay_subscription ); if ( $new_token ) { $this->logger->log( sprintf( '---- Payment token on subscription #%1$d has been updated (from %2$s to %3$s) to match the payment method on the Stripe Billing subscription.', $subscription->get_id(), $token ? $token->get_token() : 'missing', $wcpay_subscription['default_payment_method'] ) ); } } /** * Locates a payment token or creates one if it doesn't exist, then updates the subscription with the new token. * * @param WC_Subscription $subscription The subscription to add the payment token to. * @param array $wcpay_subscription The subscription data from Stripe. * * @return WC_Payment_Token|false The new payment token or false if the token couldn't be created. */ private function maybe_create_and_update_payment_token( $subscription, $wcpay_subscription ) { $token = false; $user = new WP_User( $subscription->get_user_id() ); $customer_tokens = WC_Payment_Tokens::get_tokens( [ 'user_id' => $user->ID, 'gateway_id' => WC_Payment_Gateway_WCPay::GATEWAY_ID, 'limit' => WC_Payment_Gateway_WCPay::USER_FORMATTED_TOKENS_LIMIT, ] ); foreach ( $customer_tokens as $customer_token ) { if ( $customer_token->get_token() === $wcpay_subscription['default_payment_method'] ) { $token = $customer_token; break; } } // If we didn't find a token linked to the subscription customer, create one. if ( ! $token ) { try { $token = $this->token_service->add_payment_method_to_user( $wcpay_subscription['default_payment_method'], $user ); $this->logger->log( sprintf( '---- Created a new payment token (%1$s) for subscription #%2$d.', $token->get_token(), $subscription->get_id() ) ); } catch ( \Exception $e ) { $this->logger->log( sprintf( '---- WARNING: Subscription #%1$d is missing a payment token and we failed to create one. Error: %2$s', $subscription->get_id(), $e->getMessage() ) ); return; } } // Prevent the WC_Payments_Subscriptions class from attempting to update the Stripe Billing subscription's payment method while we set the token. remove_action( 'woocommerce_payment_token_added_to_order', [ WC_Payments_Subscriptions::get_subscription_service(), 'update_wcpay_subscription_payment_method' ], 10 ); $subscription->add_payment_token( $token ); // Reattach. add_action( 'woocommerce_payment_token_added_to_order', [ WC_Payments_Subscriptions::get_subscription_service(), 'update_wcpay_subscription_payment_method' ], 10, 3 ); return $token; } /** * Prevents migrated WCPay subscription metadata being copied to subscription related orders (renewal/switch/resubscribe). * * @param array $meta_data The meta data to be copied. * @return array The meta data to be copied. */ public function exclude_migrated_meta( $meta_data ) { foreach ( $this->meta_keys_to_migrate as $key ) { unset( $meta_data[ '_migrated' . $key ] ); } return $meta_data; } /** * Logs any fatal errors that occur while processing a scheduled migrate WCPay Subscription action. * * @param string $action_id The Action Scheduler action ID. * @param array $error The error data. */ public function handle_unexpected_shutdown( $action_id, $error = null ) { $migration_args = $this->get_migration_action_args( $action_id ); if ( ! isset( $migration_args['migrate_subscription'], $migration_args['attempt'] ) ) { return; } if ( ! empty( $error['type'] ) && in_array( $error['type'], [ E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ], true ) ) { $this->logger->log( sprintf( '---- ERROR: Unexpected shutdown while migrating subscription #%1$d: %2$s in %3$s on line %4$s.', $migration_args['migrate_subscription'], $error['message'] ?? 'No message', $error['file'] ?? 'no file found', $error['line'] ?? '0' ) ); } $this->maybe_reschedule_migration( $migration_args['migrate_subscription'], $migration_args['attempt'] ); } /** * Handles any unexpected failures that occur while processing a single migration action * by logging an error message and rescheduling the action to retry. * * @param string $action_id The Action Scheduler action ID. * @param Exception $exception The exception thrown during action processing. */ public function handle_unexpected_action_failure( $action_id, $exception ) { $migration_args = $this->get_migration_action_args( $action_id ); if ( ! isset( $migration_args['migrate_subscription'], $migration_args['attempt'] ) ) { return; } $this->logger->log( sprintf( '---- ERROR: Unexpected failure while migrating subscription #%1$d: %2$s', $migration_args['migrate_subscription'], $exception->getMessage() ) ); $this->maybe_reschedule_migration( $migration_args['migrate_subscription'], $migration_args['attempt'] ); } /** * Adds a manual migration tool to WooCommerce > Status > Tools. * * This tool is only loaded on stores that have: * - WC Subscriptions extension activated * - Subscriptions with WooPayments feature disabled * - Existing WCPay Subscriptions that can be migrated * * @param array $tools List of WC debug tools. * * @return array List of WC debug tools. */ public function add_manual_migration_tool( $tools ) { if ( WC_Payments_Features::is_wcpay_subscriptions_enabled() || ! class_exists( 'WC_Subscriptions' ) ) { return $tools; } // Get number of WCPay Subscriptions that can be migrated. $wcpay_subscriptions_count = $this->get_stripe_billing_subscription_count(); if ( $wcpay_subscriptions_count < 1 ) { return $tools; } // Disable the button if a migration is currently in progress. $disabled = $this->is_migrating(); $tools['migrate_wcpay_subscriptions'] = [ 'name' => __( 'Migrate Stripe Billing subscriptions', 'woocommerce-payments' ), 'button' => $disabled ? __( 'Migration in progress', 'woocommerce-payments' ) . '…' : __( 'Migrate Subscriptions', 'woocommerce-payments' ), 'desc' => sprintf( // translators: %1$s is a new line character and %2$d is the number of subscriptions. __( 'This tool will migrate all Stripe Billing subscriptions to tokenized subscriptions with WooPayments.%1$sNumber of Stripe Billing subscriptions found: %2$d', 'woocommerce-payments' ), '<br>', $wcpay_subscriptions_count, ), 'callback' => [ $this, 'schedule_migrate_wcpay_subscriptions_action' ], 'disabled' => $disabled, 'requires_refresh' => true, ]; return $tools; } /** * Schedules the initial migration action which signals the start of the migration process. */ public function schedule_migrate_wcpay_subscriptions_action() { if ( as_next_scheduled_action( $this->scheduled_hook ) ) { return; } update_option( $this->migration_batch_identifier_option, time() ); $this->logger->log( 'Started scheduling subscription migrations.' ); $this->schedule_repair(); } /** * Gets the subscription ID and number of attempts from the action args. * * @param int $action_id The action ID to get data from. * * @return array */ private function get_migration_action_args( $action_id ) { $action = ActionScheduler_Store::instance()->fetch_action( $action_id ); if ( ! $action || ( $this->migrate_hook !== $action->get_hook() && $this->migrate_hook . '_retry' !== $action->get_hook() ) ) { return []; } $action_args = $action->get_args(); if ( ! isset( $action_args['migrate_subscription'] ) ) { return []; } return array_merge( [ 'migrate_subscription' => 0, 'attempt' => 0, ], $action_args ); } /** * Reschedules a subscription migration with increasing delays depending on number of attempts. * * After max retries, an exception is thrown if one was passed. * * @param int $subscription_id The ID of the subscription to retry. * @param int $attempt The number of times migration has been attempted. * @param \Exception|null $exception The exception thrown during migration. * * @throws \Exception If max attempts and exception passed is not null. */ public function maybe_reschedule_migration( $subscription_id, $attempt = 0, $exception = null ) { // Number of seconds to wait before retrying the migration, increasing with each attempt up to 7 attempts (12 hours). $retry_schedule = [ 60, 300, 600, 1800, HOUR_IN_SECONDS, 6 * HOUR_IN_SECONDS, 12 * HOUR_IN_SECONDS ]; // If the exception thrown contains "Skipping migration", don't reschedule the migration. if ( $exception && false !== strpos( $exception->getMessage(), 'Skipping migration' ) ) { return; } if ( isset( $retry_schedule[ $attempt ] ) && $attempt < 7 ) { $this->logger->log( sprintf( '---- Rescheduling migration of subscription #%1$d.', $subscription_id ) ); as_schedule_single_action( gmdate( 'U' ) + $retry_schedule[ $attempt ], $this->migrate_hook . '_retry', [ 'migrate_subscription' => $subscription_id, 'attempt' => $attempt + 1, ] ); } else { $this->logger->log( sprintf( '---- FAILED: Subscription #%d could not be migrated.', $subscription_id ) ); if ( $exception ) { // Before throwing the exception, remove the action_scheduler failure hook to prevent the exception being logged again. remove_action( 'action_scheduler_failed_execution', [ $this, 'handle_unexpected_action_failure' ] ); throw $exception; } } } /** * Override WCS_Background_Repairer methods. */ /** * Initialize class variables and hooks to handle scheduling and running migration hooks in the background. */ public function init() { $this->repair_hook = $this->migrate_hook; parent::init(); } /** * Schedules an individual action to migrate a subscription. * * Overrides the parent class function to make two changes: * 1. Don't schedule an action if one already exists. * 2. Schedules the migration to happen in one minute instead of in one hour. * * @param int $item The ID of the subscription to migrate. */ public function update_item( $item ) { if ( ! as_next_scheduled_action( $this->migrate_hook, [ 'migrate_subscription' => $item ] ) ) { as_schedule_single_action( gmdate( 'U' ) + 60, $this->migrate_hook, [ 'migrate_subscription' => $item ] ); } unset( $this->items_to_repair[ $item ] ); } /** * Migrates an individual subscription. * * The repair_item() function is called by the parent class when the individual scheduled action is run. * This acts as a wrapper for the migrate_wcpay_subscription() function. * * @param int $item The ID of the subscription to migrate. */ public function repair_item( $item ) { $this->migrate_wcpay_subscription( $item ); } /** * Gets a batch of 100 subscriptions to migrate. * * Because this function fetches items in batches using limit and paged query args, we need to make sure * the paging of this query is consistent regardless of whether some subscriptions have been repaired/migrated in between. * * To do this, we use the $this->migration_batch_identifier_option value to identify subscriptions previously returned by * this function that have been migrated so they will still be considered for paging. * * @param int $page The page of results to fetch. * * @return int[] The IDs of the subscriptions to migrate. */ public function get_items_to_repair( $page ) { $items_to_migrate = wcs_get_orders_with_meta_query( [ 'return' => 'ids', 'type' => 'shop_subscription', 'limit' => 100, 'status' => 'any', 'paged' => $page, 'order' => 'ASC', 'orderby' => 'ID', 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'OR', [ 'key' => WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, 'compare' => 'EXISTS', ], // We need to include subscriptions which have already been migrated as part of this migration group to make // sure correct paging is maintained. As subscriptions are migrated they would migrate the WCPay subscription ID // meta key and therefore fall out of this query's scope - messing with the paging of future queries. // Subscriptions with the `migrated_during` meta aren't expected to be returned by this query, they are included to pad out the earlier pages. [ 'key' => '_wcpay_subscription_migrated_during', 'value' => get_option( $this->migration_batch_identifier_option, 0 ), 'compare' => '=', ], ], ] ); if ( empty( $items_to_migrate ) ) { $this->logger->log( 'Finished scheduling subscription migrations.' ); } return $items_to_migrate; } /** * Gets the total number of subscriptions to migrate. * * @return int The total number of subscriptions to migrate. */ public function get_stripe_billing_subscription_count() { $result = wcs_get_orders_with_meta_query( [ 'status' => 'any', 'return' => 'ids', 'type' => 'shop_subscription', 'limit' => - 1, 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query [ 'key' => WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY, 'compare' => 'EXISTS', ], ], ] ); return is_countable( $result ) ? count( $result ) : 0; } /** * Determines if a migration is currently in progress. * * A migration is considered to be in progress if the initial migration action or an individual subscription * action (or retry) is scheduled. * * @return bool True if a migration is in progress, false otherwise. */ public function is_migrating() { return (bool) as_next_scheduled_action( $this->scheduled_hook ) || (bool) as_next_scheduled_action( $this->migrate_hook ) || (bool) as_next_scheduled_action( $this->migrate_hook . '_retry' ); } /** * Runs any actions that need to handle the completion of the migration. */ protected function unschedule_background_updates() { parent::unschedule_background_updates(); delete_option( $this->migration_batch_identifier_option ); } } includes/subscriptions/class-wc-payments-subscriptions-plugin-notice-manager.php 0000777 00000005011 15251156605 0024446 0 ustar 00 <?php /** * Class WC_Payments_Subscriptions_Plugin_Notice_Manager * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; /** * A class to handle the displaying of a warning notice when admin deactivate the WC Subscriptions extension. */ class WC_Payments_Subscriptions_Plugin_Notice_Manager { use WC_Payments_Subscriptions_Utilities; /** * Initialize the class and attach callbacks. */ public function __construct() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts_and_styles' ], 100 ); add_action( 'admin_footer', [ $this, 'output_notice_template' ] ); } /** * Determines if the current screen is the admin plugins screen. * * @return bool Whether the current request is for the admin plugins screen. */ private function is_admin_plugins_screen() { if ( ! is_admin() ) { return false; } $screen = get_current_screen(); return $screen && 'plugins' === $screen->id; } /** * Enqueues the admin scripts needed on the plugins screen. */ public function enqueue_scripts_and_styles() { if ( ! $this->is_admin_plugins_screen() || ! WC_Payments_Subscription_Service::store_has_active_wcpay_subscriptions() ) { return; } // The backbone modal requires the WC admin styles to be loaded. wp_enqueue_style( 'woocommerce_admin_styles' ); wp_register_script( 'wcpay-subscriptions-plugin', plugins_url( 'includes/subscriptions/assets/js/plugin-page.js', WCPAY_PLUGIN_FILE ), [ 'jquery', 'wc-backbone-modal' ], WCPAY_VERSION_NUMBER, true ); wp_enqueue_script( 'wcpay-subscriptions-plugin' ); WC_Payments_Utils::enqueue_style( 'wcpay-subscriptions-plugin-styles', plugins_url( 'includes/subscriptions/assets/css/plugin-page.css', WCPAY_PLUGIN_FILE ), [], WCPAY_VERSION_NUMBER, 'all' ); } /** * Enqueues templates for plugin deactivation warnings on the admin plugin screen. */ public function output_notice_template() { if ( ! $this->is_admin_plugins_screen() ) { return; } wc_get_template( 'html-subscriptions-plugin-notice.php', [], '', dirname( __DIR__ ) . '/subscriptions/templates/' ); // Load a slightly different notice for folks still using the legacy WCPay Subscriptions functionality. if ( WC_Payments::get_gateway()->is_subscriptions_plugin_active() ) { wc_get_template( 'html-woo-payments-deactivate-warning.php', [], '', dirname( __DIR__ ) . '/subscriptions/templates/' ); } else { wc_get_template( 'html-wcpay-deactivate-warning.php', [], '', dirname( __DIR__ ) . '/subscriptions/templates/' ); } } } includes/class-wc-payments-db.php 0000777 00000006007 15251156605 0013040 0 ustar 00 <?php /** * WC_Payments_DB class * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; /** * Wrapper class for accessing the database. */ class WC_Payments_DB { const META_KEY_INTENT_ID = '_intent_id'; const META_KEY_CHARGE_ID = '_charge_id'; /** * Retrieve an order from the DB using a corresponding Stripe charge ID. * * @param string $charge_id Charge ID corresponding to an order ID. * * @return boolean|WC_Order|WC_Order_Refund */ public function order_from_charge_id( $charge_id ) { $order_id = $this->order_id_from_meta_key_value( self::META_KEY_CHARGE_ID, $charge_id ); if ( $order_id ) { return $this->order_from_order_id( $order_id ); } return false; } /** * Retrieve orders from the DB using a list of corresponding Stripe charge IDs. * * @param array $charge_ids List of charge IDs corresponding to an order ID. * * @return array[] */ public function orders_with_charge_id_from_charge_ids( array $charge_ids ): array { // The order ID is saved to DB in `WC_Payment_Gateway_WCPay::process_payment()`. $orders = wc_get_orders( [ 'limit' => count( $charge_ids ), 'meta_key' => self::META_KEY_CHARGE_ID, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $charge_ids, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'meta_compare' => 'IN', ] ); return array_map( function ( WC_Order $order ): array { return [ 'order' => $order, 'charge_id' => $order->get_meta( self::META_KEY_CHARGE_ID ), ]; }, $orders ); } /** * Retrieve an order from the DB using a corresponding Stripe intent ID. * * @param string $intent_id Intent ID corresponding to an order ID. * * @return boolean|WC_Order|WC_Order_Refund */ public function order_from_intent_id( $intent_id ) { $order_id = $this->order_id_from_meta_key_value( self::META_KEY_INTENT_ID, $intent_id ); if ( $order_id ) { return $this->order_from_order_id( $order_id ); } return false; } /** * Retrieve an order ID from the DB using a meta key value pair. * * @param string $meta_key Either '_intent_id' or '_charge_id'. * @param string $meta_value Value for the meta key. * * @return null|string */ private function order_id_from_meta_key_value( $meta_key, $meta_value ) { // Don't proceed if the meta key or value is empty. if ( ! $meta_key || ! $meta_value ) { return null; } $orders = wc_get_orders( [ 'limit' => 1, 'meta_key' => $meta_key, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $meta_value, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ] ); if ( $orders && ! empty( $orders ) ) { return (string) $orders[0]->get_id(); } return null; } /** * Retrieve an order using order ID. * * @param string $order_id WC Order Id. * * @return bool|WC_Order|WC_Order_Refund */ public function order_from_order_id( $order_id ) { return wc_get_order( ( $order_id ) ); } } includes/emails/class-wc-payments-email-ipp-receipt.php 0000777 00000025201 15251156605 0017230 0 ustar 00 <?php /** * Class WC_Payments_Email_IPP_Receipt file * * @package WooCommerce\Emails * * @filter woocommerce_email_preview_dummy_order * Filters the dummy order object used for email previews. * @param WC_Order|bool $order The order object or false. * @return WC_Order The filtered order object. * * @filter woocommerce_email_preview_dummy_address * Filters the dummy address data used for email previews. * @param array $address The address data array. * @return array Modified address data array with store location details. * * @filter woocommerce_email_preview_placeholders * Filters the email preview placeholders. * @param array $placeholders Array of email preview placeholders. * @return array Modified array of placeholders with order date and number. */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( ! class_exists( 'WC_Payments_Email_IPP_Receipt' ) ) : /** * In Person Payments Receipt Email. * * An email sent to the customer when a new order is paid for with a card reader. * * @class WC_Payments_Email_IPP_Receipt * @version 2.0.0 * @package WooCommerce\Classes\Emails */ class WC_Payments_Email_IPP_Receipt extends WC_Email { /** * Merchant settings * * @var array */ public $merchant_settings = []; /** * Charge * * @var array */ public $charge = []; /** * Constructor. */ public function __construct() { $this->id = 'new_receipt'; $this->customer_email = true; $this->title = __( 'New receipt', 'woocommerce-payments' ); $this->description = __( 'New receipt emails are sent to customers when a new order is paid for with a card reader.', 'woocommerce-payments' ); $this->template_base = WCPAY_ABSPATH . 'templates/'; $this->template_html = 'emails/customer-ipp-receipt.php'; $this->template_plain = 'emails/plain/customer-ipp-receipt.php'; $this->plugin_id = 'woocommerce_woocommerce_payments_'; $this->placeholders = [ '{order_date}' => '', '{order_number}' => '', ]; // Content hooks. add_action( 'woocommerce_payments_email_ipp_receipt_store_details', [ $this, 'store_details' ], 10, 2 ); add_action( 'woocommerce_payments_email_ipp_receipt_compliance_details', [ $this, 'compliance_details' ], 10, 2 ); // Triggers for this email. add_action( 'woocommerce_payments_email_ipp_receipt_notification', [ $this, 'trigger' ], 10, 3 ); // WooCommerce Email preview filters. add_filter( 'woocommerce_email_preview_dummy_order', [ $this, 'get_preview_order' ], 10, 1 ); add_filter( 'woocommerce_email_preview_dummy_address', [ $this, 'get_preview_address' ], 10, 1 ); add_filter( 'woocommerce_email_preview_placeholders', [ $this, 'get_preview_placeholders' ], 10, 1 ); /** * Please don't move. The call to the parent constructor here is intentional. It allows this class to merge * its placeholders with the parent's and prefix the settings with its own identifier. * Moving this call to the top of the constructor will cause the placeholders to stop working and * lose the woocommerce_payments_ prefix in the settings. * * @see: WC_Email::_construct() */ parent::__construct(); } /** * Get preview order data for email preview. * * @param mixed $order The dummy order instance or false/null for non-WCPay emails. * @return mixed * @see WC_Email_Preview::get_dummy_order() */ public function get_preview_order( $order ) { if ( ! $order instanceof WC_Order ) { return $order; } $order->set_payment_method_title( __( 'WooCommerce In-Person Payments', 'woocommerce-payments' ) ); return $order; } /** * Get preview address data for email preview. * * @param array $address The address data. * @return array */ public function get_preview_address( $address ) { if ( empty( $address ) ) { $address = [ 'line1' => '123 Sample Street', 'line2' => 'Suite 100', 'city' => 'Sample City', 'state' => 'ST', 'postal_code' => '12345', 'country' => 'US', ]; } return $address; } /** * Get preview placeholders for email preview. * * @param array $placeholders The placeholders array. * @return array */ public function get_preview_placeholders( $placeholders ) { $placeholders['{order_date}'] = wc_format_datetime( new DateTime() ); $placeholders['{order_number}'] = '42'; return $placeholders; } /** * Get email subject. * * @since 3.1.0 * @return string */ public function get_default_subject(): string { return __( 'Your {site_title} Receipt', 'woocommerce-payments' ); } /** * Get email heading. * * @since 3.1.0 * @return string */ public function get_default_heading(): string { return __( 'Your receipt for order: #{order_number}', 'woocommerce-payments' ); } /** * Trigger the sending of this email. * * @param WC_Order $order The order instance. * @param array $merchant_settings The merchant settings data. * @param array $charge The charge data. */ public function trigger( WC_Order $order, array $merchant_settings, array $charge ) { $this->setup_locale(); $email_already_sent = false; if ( $order instanceof WC_Order ) { $this->object = $order; $this->recipient = $this->object->get_billing_email(); $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); $this->placeholders['{order_number}'] = $this->object->get_order_number(); $email_already_sent = $order->get_meta( '_new_receipt_email_sent' ); } $this->merchant_settings = $merchant_settings; $this->charge = $charge; if ( 'true' !== $email_already_sent && $this->is_enabled() && $this->get_recipient() ) { $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); $order->update_meta_data( '_new_receipt_email_sent', 'true' ); $order->save(); } $this->restore_locale(); } /** * Get content html. * * @return string */ public function get_content_html(): string { return wc_get_template_html( $this->template_html, [ 'order' => $this->object, 'merchant_settings' => $this->merchant_settings, 'charge' => $this->charge, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => false, 'email' => $this, ], '', WCPAY_ABSPATH . 'templates/' ); } /** * Get content plain. * * @return string */ public function get_content_plain(): string { return wc_get_template_html( $this->template_plain, [ 'order' => $this->object, 'merchant_settings' => $this->merchant_settings, 'charge' => $this->charge, 'email_heading' => $this->get_heading(), 'additional_content' => $this->get_additional_content(), 'sent_to_admin' => false, 'plain_text' => true, 'email' => $this, ], '', WCPAY_ABSPATH . 'templates/' ); } /** * Get preview merchant settings for email preview. * * @param array $settings The merchant settings. * @return array */ public function get_preview_merchant_settings( $settings ) { if ( empty( $settings ) ) { $settings = [ 'business_name' => 'Sample Store', 'support_info' => [ 'address' => [ 'line1' => '123 Sample Street', 'line2' => 'Suite 100', 'city' => 'Sample City', 'state' => 'ST', 'postal_code' => '12345', 'country' => 'US', ], 'phone' => '+1 (555) 123-4567', 'email' => 'support@samplestore.com', ], ]; } return $settings; } /** * Get store details content html * * @param array $settings The settings. * @param boolean $plain_text Whether the content type is plain text. * @return void */ public function store_details( array $settings, bool $plain_text ) { // Ensure we have all required data for preview. $settings = $this->get_preview_merchant_settings( $settings ); $template_data = [ 'business_name' => $settings['business_name'] ?? '', 'support_address' => $settings['support_info']['address'] ?? [], 'support_phone' => $settings['support_info']['phone'] ?? '', 'support_email' => $settings['support_info']['email'] ?? '', ]; if ( $plain_text ) { wc_get_template( 'emails/plain/email-ipp-receipt-store-details.php', $template_data, '', WCPAY_ABSPATH . 'templates/' ); } else { wc_get_template( 'emails/email-ipp-receipt-store-details.php', $template_data, '', WCPAY_ABSPATH . 'templates/' ); } } /** * Get preview charge data for email preview. * * @param array $charge The charge data. * @return array */ public function get_preview_charge( $charge ) { if ( empty( $charge ) ) { $charge = [ 'payment_method_details' => [ 'card_present' => [ 'brand' => 'visa', 'last4' => '4242', 'receipt' => [ 'application_preferred_name' => 'Sample App', 'dedicated_file_name' => 'Sample File', 'account_type' => 'credit', ], ], ], ]; } return $charge; } /** * Get compliance data content html * * @param array $charge The charge. * @param boolean $plain_text Whether the content type is plain text. * @return void */ public function compliance_details( array $charge, bool $plain_text ) { // Ensure we have all required data for preview. $charge = $this->get_preview_charge( $charge ); $template_data = [ 'payment_method_details' => $charge['payment_method_details']['card_present'] ?? [], 'receipt' => $charge['payment_method_details']['card_present']['receipt'] ?? [], ]; if ( $plain_text ) { wc_get_template( 'emails/plain/email-ipp-receipt-compliance-details.php', $template_data, '', WCPAY_ABSPATH . 'templates/' ); } else { wc_get_template( 'emails/email-ipp-receipt-compliance-details.php', $template_data, '', WCPAY_ABSPATH . 'templates/' ); } } /** * Default content to show below main email content. * * @return string */ public function get_default_additional_content(): string { return __( 'Thanks for using {site_url}!', 'woocommerce-payments' ); } } endif; return new WC_Payments_Email_IPP_Receipt(); includes/class-woopay-tracker.php 0000777 00000047636 15251156605 0013172 0 ustar 00 <?php /** * Class WooPay_Tracker * * @package WooCommerce\Payments */ namespace WCPay; use Jetpack_Tracks_Client; use Jetpack_Tracks_Event; use WC_Payments; use WC_Payments_Features; use WCPay\Constants\Country_Code; use WP_Error; defined( 'ABSPATH' ) || exit; // block direct access. /** * Track WooPay related events */ class WooPay_Tracker extends Jetpack_Tracks_Client { /** * WCPay user event prefix * * @var string */ private static $user_prefix = 'wcpay'; /** * WooPay admin event prefix * * @var string */ private static $admin_prefix = 'wcadmin'; /** * WCPay http interface. * * @var Object */ private $http; /** * Base URL for stats counter. * * @var string */ private static $pixel_base_url = 'https://pixel.wp.com/g.gif'; /** * Constructor. * * @param \WC_Payments_Http_Interface $http A class implementing WC_Payments_Http_Interface. */ public function __construct( $http ) { $this->http = $http; add_action( 'wp_ajax_platform_tracks', [ $this, 'ajax_tracks' ] ); add_action( 'wp_ajax_nopriv_platform_tracks', [ $this, 'ajax_tracks' ] ); add_action( 'wp_ajax_get_identity', [ $this, 'ajax_tracks_id' ] ); add_action( 'wp_ajax_nopriv_get_identity', [ $this, 'ajax_tracks_id' ] ); // Actions that should result in recorded Tracks events. add_action( 'woocommerce_after_checkout_form', [ $this, 'classic_checkout_start' ] ); add_action( 'woocommerce_after_cart', [ $this, 'classic_cart_page_view' ] ); add_action( 'woocommerce_after_single_product', [ $this, 'classic_product_page_view' ] ); add_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after', [ $this, 'blocks_checkout_start' ] ); add_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after', [ $this, 'blocks_cart_page_view' ] ); add_action( 'woocommerce_checkout_order_processed', [ $this, 'checkout_order_processed' ], 10, 2 ); add_action( 'woocommerce_store_api_checkout_order_processed', [ $this, 'checkout_order_processed' ], 10, 2 ); add_action( 'woocommerce_payments_save_user_in_woopay', [ $this, 'must_save_payment_method_to_platform' ] ); add_action( 'wp_footer', [ $this, 'add_frontend_tracks_scripts' ] ); add_action( 'before_woocommerce_pay_form', [ $this, 'pay_for_order_page_view' ] ); add_action( 'woocommerce_thankyou', [ $this, 'thank_you_page_view' ] ); // Inject tracking configuration into JS configs. add_filter( 'wcpay_payment_fields_js_config', [ $this, 'add_tracking_config_to_payment_fields' ] ); add_filter( 'wcpay_express_checkout_js_params', [ $this, 'add_tracking_config_to_express_checkout' ] ); } /** * Override jetpack-tracking's ajax handling to use internal maybe_record_event method. */ public function ajax_tracks() { // Check for nonce. if ( // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash empty( $_REQUEST['tracksNonce'] ) || ! wp_verify_nonce( $_REQUEST['tracksNonce'], 'platform_tracks_nonce' ) ) { wp_send_json_error( __( 'You aren’t authorized to do that.', 'woocommerce-payments' ), 403 ); } if ( ! isset( $_REQUEST['tracksEventName'] ) ) { wp_send_json_error( __( 'No valid event name or type.', 'woocommerce-payments' ), 403 ); } $tracks_data = []; if ( isset( $_REQUEST['tracksEventProp'] ) ) { // tracksEventProp is a JSON-encoded string. $event_prop = json_decode( wc_clean( wp_unslash( $_REQUEST['tracksEventProp'] ) ), true ); if ( is_array( $event_prop ) ) { $tracks_data = $event_prop; } } $this->maybe_record_event( sanitize_text_field( wp_unslash( $_REQUEST['tracksEventName'] ) ), $tracks_data ); wp_send_json_success(); } /** * Get tracks ID of the current user */ public function ajax_tracks_id() { $tracks_id = $this->tracks_get_identity(); if ( $tracks_id ) { wp_send_json_success( $tracks_id ); } } /** * Generic method to track user events on WooPay enabled stores. * * @param string $event name of the event. * @param array $data array of event properties. */ public function maybe_record_event( $event, $data = [] ) { // Top level events should not be namespaced. if ( '_aliasUser' !== $event ) { $event = self::$user_prefix . '_' . $event; } return $this->tracks_record_event( $event, $data ); } /** * Track shopper events with the wcpay_prefix. * * @param string $event name of the event. * @param array $data array of event properties. * @param bool $record_on_frontend whether to record the event on the frontend to prevent cache break. */ public function maybe_record_wcpay_shopper_event( $event, $data = [], $record_on_frontend = true ) { $is_admin_event = false; $track_on_all_stores = true; // Record the event immediately. if ( ! $record_on_frontend ) { // Top level events should not be namespaced. if ( '_aliasUser' !== $event ) { $event = self::$user_prefix . '_' . $event; } return $this->tracks_record_event( $event, $data, $is_admin_event, $track_on_all_stores ); } // Route the event through frontend to avoid setting cookies on page load. $data['record_event_data'] = compact( 'is_admin_event', 'track_on_all_stores' ); add_filter( 'wcpay_frontend_tracks', function ( $tracks ) use ( $event, $data ) { $tracks[] = [ 'event' => $event, 'properties' => $data, ]; return $tracks; } ); } /** * Generic method to track admin events on all WCPay stores. * * @param string $event name of the event. * @param array $data array of event properties. */ public function maybe_record_admin_event( $event, $data = [] ) { // Top level events should not be namespaced. if ( '_aliasUser' !== $event ) { $event = self::$admin_prefix . '_' . $event; } $is_admin_event = true; return $this->tracks_record_event( $event, $data, $is_admin_event ); } /** * Check whether the store country is eligible for Tracks. * * @return bool */ public function is_country_tracks_eligible() { if ( ! function_exists( 'wc_get_base_location' ) ) { return false; } $store_base_location = wc_get_base_location(); return ! empty( $store_base_location['country'] ) && Country_Code::UNITED_STATES === $store_base_location['country']; } /** * Override parent method to omit the jetpack TOS check and include custom tracking conditions. * * @param bool $is_admin_event Indicate whether the event is emitted from admin area. * @param bool $track_on_all_stores Indicate whether the event should be tracked on all stores. * * @return bool */ public function should_enable_tracking( $is_admin_event = false, $track_on_all_stores = false ) { // Allow merchants to disable all shopper tracking via filter. // The filter defaults to the WooCommerce global tracking setting. if ( ! apply_filters( 'wcpay_shopper_tracking_enabled', 'no' !== get_option( 'woocommerce_allow_tracking', '' ) ) ) { return false; } // Don't track if the gateway is not enabled. $gateway = \WC_Payments::get_gateway(); if ( ! $gateway->is_enabled() ) { return false; } // Don't track if the account is not connected. $account = WC_Payments::get_account_service(); if ( is_null( $account ) || ! $account->is_stripe_connected() ) { return false; } // Don't track any non-US stores. if ( ! $this->is_country_tracks_eligible() ) { return false; } // Always respect the user specific opt-out cookie. if ( ! empty( $_COOKIE['tk_opt-out'] ) ) { return false; } // Track all WooPay events from the admin area. if ( $is_admin_event ) { return true; } // For all other events ensure: // 1. Only site pages are tracked. // 2. Site Admin activity in site pages are not tracked. // 3. If track_on_all_stores is true, tracking is enabled regardless of WooPay eligibility. // 4. Otherwise, tracking requires WooPay to be eligible and enabled. // Track only site pages. if ( is_admin() && ! wp_doing_ajax() ) { return false; } // Don't track site admins. if ( is_user_logged_in() && in_array( 'administrator', wp_get_current_user()->roles, true ) ) { return false; } if ( $track_on_all_stores ) { return true; } // For the remaining events, don't track when woopay is disabled. $is_woopay_eligible = WC_Payments_Features::is_woopay_eligible(); // Feature flag. $is_woopay_enabled = 'yes' === $gateway->get_option( 'platform_checkout', 'no' ); if ( ! ( $is_woopay_eligible && $is_woopay_enabled ) ) { return false; } return true; } /** * Record an event in Tracks - this is the preferred way to record events from PHP. * * @param string $event_name The name of the event. * @param array $properties Custom properties to send with the event. * @param bool $is_admin_event Indicate whether the event is emitted from admin area. * @param bool $track_on_all_stores Indicate whether the event should be tracked on all stores. * * @return bool|array|\WP_Error|\Jetpack_Tracks_Event */ public function tracks_record_event( $event_name, $properties = [], $is_admin_event = false, $track_on_all_stores = false ) { $user = wp_get_current_user(); // We don't want to track user events during unit tests/CI runs. if ( $user instanceof \WP_User && 'wptests_capabilities' === $user->cap_key ) { return false; } $properties = apply_filters( 'wcpay_tracks_event_properties', $properties, $event_name ); if ( isset( $properties['record_event_data'] ) ) { if ( isset( $properties['record_event_data']['is_admin_event'] ) ) { $is_admin_event = $properties['record_event_data']['is_admin_event']; } if ( isset( $properties['record_event_data']['track_on_all_stores'] ) ) { $track_on_all_stores = $properties['record_event_data']['track_on_all_stores']; } unset( $properties['record_event_data'] ); } if ( ! $this->should_enable_tracking( $is_admin_event, $track_on_all_stores ) ) { return false; } $event_obj = $this->tracks_build_event_obj( $user, $event_name, $properties ); if ( is_wp_error( $event_obj ) ) { return $event_obj; } $pixel = $event_obj->build_pixel_url( $event_obj ); if ( ! $pixel ) { return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 ); } return self::record_pixel( $pixel ); } /** * Procedurally build a Tracks Event Object. * * @param \WP_User $user WP_user object. * @param string $event_name The name of the event. * @param array $properties Custom properties to send with the event. * * @return \Jetpack_Tracks_Event|\WP_Error */ private function tracks_build_event_obj( $user, $event_name, $properties = [] ) { $identity = $this->tracks_get_identity(); $site_url = get_option( 'siteurl' ); $properties['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : ''; $properties['blog_url'] = $site_url; $properties['blog_id'] = \Jetpack_Options::get_option( 'id' ); $properties['user_lang'] = $user->get( 'WPLANG' ); $properties['store_id'] = $this->get_wc_store_id(); // Add event property for test mode vs. live mode events. $properties['test_mode'] = WC_Payments::mode()->is_test() ? 1 : 0; $properties['wcpay_version'] = WCPAY_VERSION_NUMBER; // Add client's user agent to the event properties. if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) { $properties['_via_ua'] = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ); } $blog_details = [ 'blog_lang' => isset( $properties['blog_lang'] ) ? $properties['blog_lang'] : get_bloginfo( 'language' ), ]; $timestamp = round( microtime( true ) * 1000 ); $timestamp_string = is_string( $timestamp ) ? $timestamp : number_format( $timestamp, 0, '', '' ); /** * Ignore incorrect argument definition in Jetpack_Tracks_Event. */ return new \Jetpack_Tracks_Event( array_merge( $blog_details, (array) $properties, $identity, [ '_en' => $event_name, '_ts' => $timestamp_string, ] ) ); } /** * Returns WC store_id value, if available. * store_id introduced in WC 8.4. * * @return string|null */ public function get_wc_store_id() { if ( defined( '\WC_Install::STORE_ID_OPTION' ) ) { return get_option( \WC_Install::STORE_ID_OPTION, null ); } return null; } /** * Get the identity to send to tracks. * * @return array $identity */ public function tracks_get_identity() { $user_id = get_current_user_id(); // Meta is set, and user is still connected. Use WPCOM ID. $wpcom_id = get_user_meta( $user_id, 'jetpack_tracks_wpcom_id', true ); if ( $wpcom_id && $this->http->is_user_connected( $user_id ) ) { return [ '_ut' => 'wpcom:user_id', '_ui' => $wpcom_id, ]; } // User is connected, but no meta is set yet. Use WPCOM ID and set meta. if ( $this->http->is_user_connected( $user_id ) ) { $wpcom_user_data = $this->http->get_connected_user_data( $user_id ); update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_user_data['ID'] ); return [ '_ut' => 'wpcom:user_id', '_ui' => $wpcom_user_data['ID'], ]; } // User isn't linked at all. Fall back to anonymous ID. $anon_id = get_user_meta( $user_id, 'jetpack_tracks_anon_id', true ); if ( ! $anon_id ) { $anon_id = \Jetpack_Tracks_Client::get_anon_id(); add_user_meta( $user_id, 'jetpack_tracks_anon_id', $anon_id, false ); } return [ '_ut' => 'anon', '_ui' => $anon_id, ]; } /** * Record a Tracks event that the classic checkout page has loaded. */ public function classic_checkout_start() { $is_woopay_enabled = WC_Payments_Features::is_woopay_enabled(); $this->maybe_record_wcpay_shopper_event( 'checkout_page_view', [ 'theme_type' => 'short_code', 'woopay_enabled' => $is_woopay_enabled, ] ); } /** * Record a Tracks event that the blocks checkout page has loaded. */ public function blocks_checkout_start() { $is_woopay_enabled = WC_Payments_Features::is_woopay_enabled(); $this->maybe_record_wcpay_shopper_event( 'checkout_page_view', [ 'theme_type' => 'blocks', 'woopay_enabled' => $is_woopay_enabled, ] ); } /** * Record a Tracks event that the classic cart page has loaded. */ public function classic_cart_page_view() { $this->maybe_record_wcpay_shopper_event( 'cart_page_view', [ 'theme_type' => 'short_code', ] ); } /** * Record a Tracks event that the blocks cart page has loaded. */ public function blocks_cart_page_view() { $this->maybe_record_wcpay_shopper_event( 'cart_page_view', [ 'theme_type' => 'blocks', ] ); } /** * Record a Tracks event that the classic cart product has loaded. */ public function classic_product_page_view() { $this->maybe_record_wcpay_shopper_event( 'product_page_view', [ 'theme_type' => 'short_code', ] ); } /** * Record a Tracks event that the pay-for-order page has loaded. */ public function pay_for_order_page_view() { $this->maybe_record_wcpay_shopper_event( 'pay_for_order_page_view' ); } /** * Bump a counter. No user identifiable information is sent. * * @param string $group The group to bump the stat in. * @param string $stat_name The name of the stat to bump. * * @return bool */ public function bump_stats( $group, $stat_name ) { $is_admin_event = false; $track_on_all_stores = true; if ( ! $this->should_enable_tracking( $is_admin_event, $track_on_all_stores ) ) { return false; } if ( WC_Payments::mode()->is_test() ) { return false; } $pixel_url = sprintf( self::$pixel_base_url . '?v=wpcom-no-pv&x_%s=%s', $group, $stat_name ); $response = wp_remote_get( esc_url_raw( $pixel_url ) ); if ( is_wp_error( $response ) ) { return false; } if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return false; } return true; } /** * Record that the order has been processed. * * @param int $order_id The ID of the order. */ public function checkout_order_processed( $order_id ) { $payment_gateway = wc_get_payment_gateway_by_order( $order_id ); $properties = [ 'payment_title' => 'other' ]; // If the order was placed using WooCommerce Payments, record the payment title using Tracks. if ( isset( $payment_gateway->id ) && strpos( $payment_gateway->id, 'woocommerce_payments' ) === 0 ) { $order = wc_get_order( $order_id ); $payment_title = $order->get_payment_method_title(); $properties = [ 'payment_title' => $payment_title ]; $is_woopay_order = ( isset( $_SERVER['HTTP_USER_AGENT'] ) && 'WooPay' === $_SERVER['HTTP_USER_AGENT'] ); // Don't track WooPay orders. They will be tracked on WooPay side with more details. if ( ! $is_woopay_order ) { $this->maybe_record_wcpay_shopper_event( 'checkout_order_placed', $properties, false ); } // If the order was placed using a different payment gateway, just increment a counter. } else { $this->bump_stats( 'wcpay_order_completed_gateway', 'other' ); } } /** * Record a Tracks event that user chose to save payment information in woopay. */ public function must_save_payment_method_to_platform() { $this->maybe_record_event( 'woopay_registered', [ 'source' => 'checkout', ] ); } /** * Record a Tracks event that Thank you page was viewed for a WCPay order. * * @param int $order_id The ID of the order. * @return void */ public function thank_you_page_view( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order || 'woocommerce_payments' !== $order->get_payment_method() ) { return; } $this->maybe_record_wcpay_shopper_event( 'order_success_page_view' ); } /** * Record a Tracks event that the WooPay express button locations has been updated. * * @param array $all_locations All pages where WooPay express button can be enabled. * @param array $platform_checkout_enabled_locations pages where WooPay express button is enabled. * * @return void */ public function woopay_locations_updated( $all_locations, $platform_checkout_enabled_locations ) { $props = []; foreach ( array_keys( $all_locations ) as $location ) { $key = $location . '_enabled'; if ( in_array( $location, $platform_checkout_enabled_locations, true ) ) { $props[ $key ] = true; } else { $props[ $key ] = false; } } $this->maybe_record_admin_event( 'woopay_express_button_locations_updated', $props ); } /** * Add tracking configuration to payment fields JS config. * * @param array $config The payment fields JS config. * @return array The modified config. */ public function add_tracking_config_to_payment_fields( $config ) { $config['isShopperTrackingEnabled'] = $this->should_enable_tracking( false, false ); return $config; } /** * Add tracking configuration to express checkout JS params. * * @param array $params The express checkout JS params. * @return array The modified params. */ public function add_tracking_config_to_express_checkout( $params ) { $params['is_shopper_tracking_enabled'] = $this->should_enable_tracking( false, false ); return $params; } /** * Add front-end tracks scripts to prevent cache break. * * @return void */ public function add_frontend_tracks_scripts() { $frontent_tracks = apply_filters( 'wcpay_frontend_tracks', [] ); if ( count( $frontent_tracks ) === 0 ) { return; } WC_Payments::register_script_with_dependencies( 'wcpay-frontend-tracks', 'dist/frontend-tracks' ); // Define wcpayConfig before the frontend tracks script if it hasn't been defined yet. $wcpay_config = rawurlencode( wp_json_encode( WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() ) ); wp_add_inline_script( 'wcpay-frontend-tracks', " var wcpayConfig = wcpayConfig || JSON.parse( decodeURIComponent( '" . esc_js( $wcpay_config ) . "' ) ); ", 'before' ); wp_localize_script( 'wcpay-frontend-tracks', 'wcPayFrontendTracks', $frontent_tracks ); wp_enqueue_script( 'wcpay-frontend-tracks' ); } } includes/class-wc-payments-order-service.php 0000777 00000251645 15251156605 0015236 0 ustar 00 <?php /** * Class WC_Payments_Order_Service * * @package WooCommerce\Payments */ use WCPay\Constants\Fraud_Meta_Box_Type; use WCPay\Constants\Order_Status; use WCPay\Constants\Intent_Status; use WCPay\Constants\Payment_Method; use WCPay\Constants\Refund_Status; use WCPay\Constants\Refund_Failure_Reason; use WCPay\Exceptions\Order_Not_Found_Exception; use WCPay\Fraud_Prevention\Models\Rule; use WCPay\Logger; use WCPay\Core\Server\Request\Get_Intention; use WCPay\Core\Server\Request\Cancel_Intention; use WCPay\Core\Server\Request\Capture_Intention; defined( 'ABSPATH' ) || exit; /** * Class handling order functionality. */ class WC_Payments_Order_Service { const ADD_FEE_BREAKDOWN_TO_ORDER_NOTES = 'wcpay_add_fee_breakdown_to_order_notes'; /** * Meta key used to store intent Id. * * @const string */ const INTENT_ID_META_KEY = '_intent_id'; /** * Meta key used to store payment method Id. * * @const string */ const PAYMENT_METHOD_ID_META_KEY = '_payment_method_id'; /** * Meta key used to store charge Id. * * @const string */ const CHARGE_ID_META_KEY = '_charge_id'; /** * Meta key used to store intention status. * * @const string */ const INTENTION_STATUS_META_KEY = '_intention_status'; /** * Meta key used to store the charge risk level. * * @const string */ const CHARGE_RISK_LEVEL_META_KEY = '_charge_risk_level'; /** * Meta key used to store customer Id. * * @const string */ const CUSTOMER_ID_META_KEY = '_stripe_customer_id'; /** * Meta key used to store WCPay fraud meta box type. * * @const string */ const WCPAY_FRAUD_META_BOX_TYPE_META_KEY = '_wcpay_fraud_meta_box_type'; /** * Meta key used to store WCPay fraud outcome status. * * @const string */ const WCPAY_FRAUD_OUTCOME_STATUS_META_KEY = '_wcpay_fraud_outcome_status'; /** * Meta key used to store WCPay intent currency. * * @const string */ const WCPAY_INTENT_CURRENCY_META_KEY = '_wcpay_intent_currency'; /** * Meta key used to store WCPay refund id. * * @const string */ const WCPAY_REFUND_ID_META_KEY = '_wcpay_refund_id'; /** * Meta key used to store WCPay refund transaction id. * * @const string */ const WCPAY_REFUND_TRANSACTION_ID_META_KEY = '_wcpay_refund_transaction_id'; /** * Meta key used to store WCPay refund status. * * @const string */ const WCPAY_REFUND_STATUS_META_KEY = '_wcpay_refund_status'; /** * Meta key used to store WCPay transaction fee. * * @const string */ const WCPAY_TRANSACTION_FEE_META_KEY = '_wcpay_transaction_fee'; /** * Meta key used to store the mode, either 'test', or 'prod' of order. * * @see Order_Mode * * @const string */ const WCPAY_MODE_META_KEY = '_wcpay_mode'; /** * Meta key used to store payment transaction Id. * * @const string */ const WCPAY_PAYMENT_TRANSACTION_ID_META_KEY = '_wcpay_payment_transaction_id'; /** * Meta key used to store the Multibanco entity. * * @const string */ const WCPAY_MULTIBANCO_ENTITY_META_KEY = '_wcpay_multibanco_entity'; /** * Meta key used to store the Multibanco reference. * * @const string */ const WCPAY_MULTIBANCO_REFERENCE_META_KEY = '_wcpay_multibanco_reference'; /** * Meta key used to store the Multibanco expiry. * * @const string */ const WCPAY_MULTIBANCO_EXPIRY_META_KEY = '_wcpay_multibanco_expiry'; /** * Meta key used to store the Multibanco URL. * * @const string */ const WCPAY_MULTIBANCO_URL_META_KEY = '_wcpay_multibanco_url'; /** * Meta key for cached payment method details. * * @const string */ const PAYMENT_METHOD_DETAILS_META_KEY = '_wcpay_payment_method_details'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ protected $api_client; /** * WC_Payments_Order_Service constructor. * * @param WC_Payments_API_Client $api_client - WooCommerce Payments API client. */ public function __construct( WC_Payments_API_Client $api_client ) { $this->api_client = $api_client; } /** * Parse the payment intent data and add any necessary notes to the order and update the order status accordingly. * * @param WC_Order $order The order to update. * @param WC_Payments_API_Abstract_Intention $intent Setup or payment intent to pull the data from. */ public function update_order_status_from_intent( $order, $intent ) { $intent_data = $this->get_intent_data( $intent ); if ( ! isset( $intent_data['intent_id'] ) || ! $this->order_prepared_for_processing( $order, $intent_data['intent_id'] ) ) { return; } switch ( $intent_data['intent_status'] ) { case Intent_Status::CANCELED: $this->mark_payment_capture_cancelled( $order, $intent_data ); break; case Intent_Status::SUCCEEDED: if ( Intent_Status::REQUIRES_CAPTURE === $this->get_intention_status_for_order( $order ) ) { $this->mark_payment_capture_completed( $order, $intent ); } else { $this->mark_payment_completed( $order, $intent_data ); } break; case Intent_Status::PROCESSING: case Intent_Status::REQUIRES_CAPTURE: if ( Rule::FRAUD_OUTCOME_REVIEW === $intent_data['fraud_outcome'] ) { $this->mark_order_held_for_review_for_fraud( $order, $intent_data ); } else { $this->mark_payment_authorized( $order, $intent_data ); } break; case Intent_Status::REQUIRES_ACTION: case Intent_Status::REQUIRES_PAYMENT_METHOD: if ( ! empty( $intent_data['error'] ) ) { $this->unlock_order_payment( $order ); $this->mark_payment_failed( $order, $intent_data['intent_id'], $intent_data['intent_status'], $intent_data['charge_id'], $intent_data['error']['message'] ); } elseif ( in_array( $intent->get_payment_method_type(), Payment_Method::OFFLINE_PAYMENT_METHODS, true ) ) { $this->mark_payment_on_hold( $order, $intent_data ); } else { $this->mark_payment_started( $order, $intent_data ); } break; default: Logger::error( 'Uncaught payment intent status of ' . $intent_data['intent_status'] . ' passed for order id: ' . $order->get_id() ); break; } $this->complete_order_processing( $order ); } /** * Handles the order state when a payment is captured successfully. * Unlike `update_order_status_from_intent`, this method does not check the current order status or skip processing * if the order is already in the "processing" state. This ensures the order status is updated correctly upon a * successful capture, preventing issues where the capture is not reflected in the order details or transaction screens * due to the order status being in the processing state. * * @param WC_Order $order The order to update. * @param WC_Payments_API_Abstract_Intention $intent The intent object containing payment or setup data. */ public function process_captured_payment( $order, $intent ) { $this->mark_payment_capture_completed( $order, $intent ); $this->complete_order_processing( $order, $intent->get_status() ); } /** * Updates an order to failed status, while adding a note with a link to the transaction. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * @param string $intent_status The status of the intent related to this order. * @param string $charge_id The charge ID related to the intent/order. * @param string $message Optional message to add to the failed note. * * @return void */ public function mark_payment_failed( $order, $intent_id, $intent_status, $charge_id, $message = '' ) { if ( ! $this->order_prepared_for_processing( $order, $intent_id ) ) { return; } $note = $this->generate_payment_failure_note( $intent_id, $charge_id, $message, $this->get_order_amount( $order ) ); if ( $this->order_note_exists( $order, $note ) || $order->has_status( [ Order_Status::FAILED ] ) ) { $this->complete_order_processing( $order ); return; } $this->update_order_status( $order, Order_Status::FAILED ); $order->add_order_note( $note ); $this->complete_order_processing( $order, $intent_status ); } /** * Leaves order in current status (should be on-hold), adds a note with a link to the transaction. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * @param string|null $intent_status The status of the intent related to this order. * @param string $charge_id The charge ID related to the intent/order. * @param string $message Optional message to add to the note. * * @return void */ public function mark_payment_capture_failed( $order, $intent_id, $intent_status, $charge_id, $message = '' ) { if ( ! $this->order_prepared_for_processing( $order, $intent_id ) ) { return; } $note = $this->generate_capture_failed_note( $order, $intent_id, $charge_id, $message ); if ( $this->order_note_exists( $order, $note ) ) { $this->complete_order_processing( $order ); return; } if ( Rule::FRAUD_OUTCOME_REVIEW === $this->get_fraud_outcome_status_for_order( $order ) ) { $this->set_fraud_meta_box_type_for_order( $order, Fraud_Meta_Box_Type::REVIEW_FAILED ); } $order->add_order_note( $note ); $this->complete_order_processing( $order, $intent_status ); } /** * Update an order to failed status, and add note with a link to the transaction. * * Context - when a Payment Intent expires. Changing the status to failed will enable the buyer to re-attempt payment. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * @param string $intent_status The status of the intent related to this order. * @param string $charge_id The charge ID related to the intent/order. * * @return void */ public function mark_payment_capture_expired( $order, $intent_id, $intent_status, $charge_id ) { if ( ! $this->order_prepared_for_processing( $order, $intent_id ) ) { return; } $note = $this->generate_capture_expired_note( $intent_id, $charge_id ); if ( $this->order_note_exists( $order, $note ) ) { $this->complete_order_processing( $order ); return; } if ( Rule::FRAUD_OUTCOME_REVIEW === $this->get_fraud_outcome_status_for_order( $order ) ) { $this->set_fraud_meta_box_type_for_order( $order, Fraud_Meta_Box_Type::REVIEW_EXPIRED ); } $this->update_order_status( $order, Order_Status::FAILED ); $order->add_order_note( $note ); $this->complete_order_processing( $order, $intent_status ); } /** * Leaves order status as Pending, adds fraud meta data, and adds the fraud blocked note. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * @param string $intent_status The status of the intent related to this order. * * @return void */ public function mark_order_blocked_for_fraud( $order, $intent_id, $intent_status ) { if ( ! $this->order_prepared_for_processing( $order, $intent_id ) ) { return; } $note = $this->generate_fraud_blocked_note( $order ); if ( $this->order_note_exists( $order, $note ) ) { $this->complete_order_processing( $order ); return; } $this->set_fraud_outcome_status_for_order( $order, Rule::FRAUD_OUTCOME_BLOCK ); $this->set_fraud_meta_box_type_for_order( $order, Fraud_Meta_Box_Type::BLOCK ); $order->add_order_note( $note ); $this->complete_order_processing( $order, $intent_status ); } /** * Updates the order to on-hold status and adds a note about the dispute. * * @param WC_Order $order Order object. * @param string $charge_id The ID of the disputed charge associated with this order. * @param string $amount The disputed amount – formatted currency value. * @param string $reason The reason for the dispute – human-readable text. * @param string $due_by The deadline for responding to the dispute - formatted date string. * @param string $status The status of the dispute. * * @return void */ public function mark_payment_dispute_created( $order, $charge_id, $amount, $reason, $due_by, $status = '' ) { if ( ! is_a( $order, 'WC_Order' ) ) { return; } $is_inquiry = strpos( $status, 'warning_' ) === 0; $note = $this->generate_dispute_created_note( $charge_id, $amount, $reason, $due_by, $is_inquiry ); if ( $this->order_note_exists( $order, $note ) ) { return; } $this->update_order_status( $order, Order_Status::ON_HOLD ); $order->add_order_note( $note ); $order->save(); } /** * Updates the order status based on dispute status and adds a note about the dispute. * * @param WC_Order $order Order object. * @param string $charge_id The ID of the disputed charge associated with this order. * @param string $status The status of the dispute. * * @return void */ public function mark_payment_dispute_closed( $order, $charge_id, $status ) { if ( ! is_a( $order, 'WC_Order' ) ) { return; } $is_inquiry = strpos( $status, 'warning_' ) === 0; $note = $this->generate_dispute_closed_note( $charge_id, $status, $is_inquiry ); if ( $this->order_note_exists( $order, $note ) ) { return; } // Order `completed` and `refunded` emails should both be blocked when disputes are closed. add_filter( 'woocommerce_email_enabled_customer_completed_order', '__return_false' ); add_filter( 'woocommerce_email_enabled_customer_refunded_order', '__return_false' ); add_filter( 'woocommerce_email_enabled_customer_completed_renewal_order', '__return_false' ); if ( 'lost' === $status ) { wc_create_refund( [ 'amount' => $order->get_total(), 'reason' => __( 'Dispute lost.', 'woocommerce-payments' ), 'order_id' => $order->get_id(), 'line_items' => $order->get_items(), ] ); } else { // TODO: This should revert to the status the order was in before the dispute was created. $this->update_order_status( $order, Order_Status::COMPLETED ); $order->save(); } // Restore completed and refunded order emails. remove_filter( 'woocommerce_email_enabled_customer_completed_order', '__return_false' ); remove_filter( 'woocommerce_email_enabled_customer_refunded_order', '__return_false' ); remove_filter( 'woocommerce_email_enabled_customer_completed_renewal_order', '__return_false' ); $order->add_order_note( $note ); } /** * Updates a terminal order to completed status, while adding a note with a link to the transaction. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * @param string $intent_status The status of the intent related to this order. * * @return void */ public function mark_terminal_payment_completed( $order, $intent_id, $intent_status ) { /** * Filters the order status value after a successful terminal payment. * * This filter can be used to override the order status from `completed` to `processing` after a successful terminal charge. * * @since 6.7.0 */ $order_status = apply_filters( 'wcpay_terminal_payment_completed_order_status', Order_Status::COMPLETED ); $this->update_order_status( $order, $order_status, $intent_id ); $this->set_fraud_meta_box_type_for_order( $order, Fraud_Meta_Box_Type::TERMINAL_PAYMENT ); $this->complete_order_processing( $order, $intent_status ); } /** * Mark terminal payment failed function. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * @param string $intent_status The status of the intent related to this order. * @param string $charge_id The charge ID related to the intent/order. * @param string $message Optional message to add to the failed note. * * @return void */ public function mark_terminal_payment_failed( $order, string $intent_id, string $intent_status, string $charge_id, string $message ) { if ( ! $this->order_prepared_for_processing( $order, $intent_id ) ) { return; } $order_status_before_update = $order->get_status(); $this->update_order_status( $order, Order_Status::FAILED ); $note = $this->generate_terminal_payment_failure_note( $intent_id, $charge_id, $message, $this->get_order_amount( $order ) ); if ( $this->order_note_exists( $order, $note ) ) { $this->complete_order_processing( $order ); return; } $order->add_order_note( $note ); $this->complete_order_processing( $order, $intent_status ); // Trigger the failed order status hook to send notifications etc only if the order status was not already failed to avoid duplicate notifications. if ( Order_Status::FAILED === $order_status_before_update ) { do_action( 'woocommerce_order_status_pending_to_failed_notification', $order->get_id(), $order ); do_action( 'woocommerce_order_status_failed_notification', $order->get_id(), $order ); } } /** * Check if a note content has already existed in the order. * * @param WC_Order $order The order object to add the note. * @param string $note_content Note content. * * @return bool true if the note content exists, false otherwise. */ public function order_note_exists( WC_Order $order, string $note_content ): bool { // Get current notes of the order. $current_notes = wc_get_order_notes( [ 'order_id' => $order->get_id() ] ); foreach ( $current_notes as $current_note ) { if ( $current_note->content === $note_content ) { return true; } } return false; } /** * Adds a note with the fee breakdown for the order. * * @param string $order_id WC Order Id. * @param string $intent_id The intent id for the payment. * @param bool $is_test_mode Whether to run the CRON job in test mode. */ public function add_fee_breakdown_to_order_notes( $order_id, $intent_id, $is_test_mode = false ) { // Since this CRON job may have been created in test_mode, when the CRON job runs, it // may lose the test_mode context. So, instead, we pass that context when creating // the CRON job and apply the context here. $apply_test_mode_context = function () use ( $is_test_mode ) { return $is_test_mode; }; add_filter( 'wcpay_test_mode', $apply_test_mode_context ); $order = wc_get_order( $order_id ); try { $events = $this->api_client->get_timeline( $intent_id ); $captured_event = current( array_filter( $events['data'], function ( array $event ) { return 'captured' === $event['type']; } ) ); $details = ( new WC_Payments_Captured_Event_Note( $captured_event ) )->generate_html_note(); // Add fee breakdown details to the note. $title = WC_Payments_Utils::esc_interpolated_html( // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings __( '<strong>Fee details:</strong>', 'woocommerce-payments' ), [ 'strong' => '<strong>', ] ); $note = $title . $details; // Update the order with the new note. $order->add_order_note( $note ); $order->save(); } catch ( Exception $e ) { Logger::log( sprintf( 'Can not generate the detailed note for intent_id %1$s. Reason: %2$s', $intent_id, $e->getMessage() ) ); } } /** * Get the payment metadata for intent id. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_intent_id_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::INTENT_ID_META_KEY, true ); } /** * Set the payment metadata for intent id. * * @param WC_Order $order The order object. * @param string $intent_id The value to be set. * * @throws Order_Not_Found_Exception */ public function set_intent_id_for_order( $order, $intent_id ) { $order = $this->get_order( $order ); $order->update_meta_data( self::INTENT_ID_META_KEY, $intent_id ); $order->save_meta_data(); /** * Hook: When the order meta data _intent_id is updated. * * @since 5.4.0 */ do_action( 'wcpay_order_intent_id_updated' ); } /** * Get the payment metadata for payment method id. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_payment_method_id_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::PAYMENT_METHOD_ID_META_KEY, true ); } /** * Set the payment metadata for payment method id. * * @param mixed $order The order. * @param string $payment_method_id The value to be set. * * @throws Order_Not_Found_Exception */ public function set_payment_method_id_for_order( $order, $payment_method_id ) { $order = $this->get_order( $order ); $order->update_meta_data( self::PAYMENT_METHOD_ID_META_KEY, $payment_method_id ); $order->save_meta_data(); /** * Hook: When the order meta data _payment_method_id is updated. * * @since 5.4.0 */ do_action( 'wcpay_order_payment_method_id_updated' ); } /** * Set the payment metadata for charge id. * * @param mixed $order The order. * @param string $charge_id The value to be set. * * @throws Order_Not_Found_Exception */ public function set_charge_id_for_order( $order, $charge_id ) { $order = $this->get_order( $order ); $order->update_meta_data( self::CHARGE_ID_META_KEY, $charge_id ); $order->save_meta_data(); } /** * Set the payment metadata for payment transaction id. * * @param mixed $order The order. * @param string $payment_transaction_id The value to be set. * * @throws Order_Not_Found_Exception */ public function set_payment_transaction_id_for_order( $order, $payment_transaction_id ) { if ( ! isset( $payment_transaction_id ) || null === $payment_transaction_id ) { return; } $order = $this->get_order( $order ); $order->update_meta_data( self::WCPAY_PAYMENT_TRANSACTION_ID_META_KEY, $payment_transaction_id ); $order->save_meta_data(); } /** * Set the payment metadata for risk level. * * @param mixed $order The order. * @param string $risk_level The value to be set. * * @throws Order_Not_Found_Exception */ public function set_charge_risk_level_for_order( $order, $risk_level ) { if ( ! isset( $risk_level ) || null === $risk_level ) { return; } $order = $this->get_order( $order ); $order->update_meta_data( self::CHARGE_RISK_LEVEL_META_KEY, $risk_level ); $order->save_meta_data(); } /** * Get the risk level for an order. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_charge_risk_level_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::CHARGE_RISK_LEVEL_META_KEY, true ); } /** * Get the payment metadata for charge id. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_charge_id_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::CHARGE_ID_META_KEY, true ); } /** * Set the payment metadata for intention status. * * @param mixed $order The order. * @param string $intention_status The value to be set. * * @throws Order_Not_Found_Exception */ public function set_intention_status_for_order( $order, $intention_status ) { $order = $this->get_order( $order ); $order->update_meta_data( self::INTENTION_STATUS_META_KEY, $intention_status ); $order->save_meta_data(); } /** * Get the payment metadata for intention status. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_intention_status_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::INTENTION_STATUS_META_KEY, true ); } /** * Checks if order has an open (uncaptured) authorization. * * @param mixed $order The order Id or order object. * * @return bool * * @throws Order_Not_Found_Exception */ public function has_open_authorization( $order ): bool { $order = $this->get_order( $order ); return Intent_Status::REQUIRES_CAPTURE === $order->get_meta( self::INTENTION_STATUS_META_KEY, true ); } /** * Set the payment metadata for customer id. * * @param mixed $order The order. * @param string $customer_id The value to be set. * * @throws Order_Not_Found_Exception */ public function set_customer_id_for_order( $order, $customer_id ) { $order = $this->get_order( $order ); $order->update_meta_data( self::CUSTOMER_ID_META_KEY, $customer_id ); $order->save_meta_data(); } /** * Get the payment metadata for customer id. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_customer_id_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::CUSTOMER_ID_META_KEY, true ); } /** * Set the payment metadata for intent currency. * * @param mixed $order The order. * @param string $wcpay_intent_currency The value to be set. * * @throws Order_Not_Found_Exception */ public function set_wcpay_intent_currency_for_order( $order, $wcpay_intent_currency ) { $order = $this->get_order( $order ); $order->update_meta_data( self::WCPAY_INTENT_CURRENCY_META_KEY, $wcpay_intent_currency ); $order->save_meta_data(); } /** * Get the payment metadata for intent currency. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_wcpay_intent_currency_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::WCPAY_INTENT_CURRENCY_META_KEY, true ); } /** * Set WCPay refund ID as metadata for refund object. * * @param WC_Order_Refund $wc_refund The refund instance. * @param string $wcpay_refund_id The value to be set. * * @throws Order_Not_Found_Exception */ public function set_wcpay_refund_id_for_refund( $wc_refund, $wcpay_refund_id ) { $wc_refund = $this->get_order( $wc_refund ); $wc_refund->update_meta_data( self::WCPAY_REFUND_ID_META_KEY, $wcpay_refund_id ); $wc_refund->save_meta_data(); } /** * Set the payment metadata for refund transaction id. * * @param WC_Order_Refund $order The order. * @param string $wcpay_transaction_id The value to be set. * * @throws Order_Not_Found_Exception */ public function set_wcpay_refund_transaction_id_for_order( WC_Order_Refund $order, string $wcpay_transaction_id ) { $order = $this->get_order( $order ); $order->update_meta_data( self::WCPAY_REFUND_TRANSACTION_ID_META_KEY, $wcpay_transaction_id ); $order->save_meta_data(); } /** * Get the payment metadata for refund id. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_wcpay_refund_id_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::WCPAY_REFUND_ID_META_KEY, true ); } /** * Set the payment metadata for refund status. * * @param mixed $order The order. * @param string $wcpay_refund_status The value to be set. * * @throws Order_Not_Found_Exception */ public function set_wcpay_refund_status_for_order( $order, $wcpay_refund_status ) { $order = $this->get_order( $order ); $order->update_meta_data( self::WCPAY_REFUND_STATUS_META_KEY, $wcpay_refund_status ); $order->save_meta_data(); } /** * Get the payment metadata for refund status. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_wcpay_refund_status_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::WCPAY_REFUND_STATUS_META_KEY, true ); } /** * Set the fraud_outcome_status for an order. * * @param mixed $order The order. * @param string $fraud_outcome_status The value to be set. * * @throws Order_Not_Found_Exception */ public function set_fraud_outcome_status_for_order( $order, $fraud_outcome_status ) { $order = $this->get_order( $order ); $order->update_meta_data( self::WCPAY_FRAUD_OUTCOME_STATUS_META_KEY, $fraud_outcome_status ); $order->save_meta_data(); } /** * Get the fraud_outcome_status for an order. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_fraud_outcome_status_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::WCPAY_FRAUD_OUTCOME_STATUS_META_KEY, true ); } /** * Set the fraud_meta_box_type for an order. * * @param mixed $order The order. * @param string $fraud_meta_box_type The value to be set. * * @throws Order_Not_Found_Exception */ public function set_fraud_meta_box_type_for_order( $order, $fraud_meta_box_type ) { $order = $this->get_order( $order ); $order->update_meta_data( self::WCPAY_FRAUD_META_BOX_TYPE_META_KEY, $fraud_meta_box_type ); $order->save_meta_data(); } /** * Get the fraud_meta_box_type for an order. * * @param mixed $order The order Id or order object. * * @return string * * @throws Order_Not_Found_Exception */ public function get_fraud_meta_box_type_for_order( $order ): string { $order = $this->get_order( $order ); return $order->get_meta( self::WCPAY_FRAUD_META_BOX_TYPE_META_KEY, true ); } /** * Given the payment intent data, adds it to the given order as metadata and parses any notes that need to be added * * @param WC_Order $order The order. * @param WC_Payments_API_Payment_Intention|WC_Payments_API_Setup_Intention $intent The payment or setup intention object. * @param bool $allow_update_on_success Whether the payment is being changed for a subscription. * * @throws Order_Not_Found_Exception */ public function attach_intent_info_to_order( WC_Order $order, $intent, $allow_update_on_success = false ) { // We don't want to allow metadata for a successful payment to be disrupted (except for when changing payment method for subscription or renewing subscription). if ( Intent_Status::SUCCEEDED === $this->get_intention_status_for_order( $order ) && ! $allow_update_on_success ) { return; } // first, let's prepare all the metadata needed for refunds, required for status change etc. $intent_id = $intent->get_id(); $intent_status = $intent->get_status(); $payment_method = $intent->get_payment_method_id(); $customer_id = $intent->get_customer_id(); $currency = $intent instanceof WC_Payments_API_Payment_Intention ? $intent->get_currency() : $order->get_currency(); $charge = $intent instanceof WC_Payments_API_Payment_Intention ? $intent->get_charge() : null; $charge_id = $charge ? $charge->get_id() : null; $payment_transaction = $charge ? $charge->get_balance_transaction() : null; $payment_transaction_id = $payment_transaction['id'] ?? ''; $outcome = $charge ? $charge->get_outcome() : null; $risk_level = $outcome ? $outcome['risk_level'] : null; // next, save it in order meta. $this->attach_intent_info_to_order__legacy( $order, $intent_id, $intent_status, $payment_method, $customer_id, $charge_id, $currency, $payment_transaction_id, $risk_level ); // Store payment method details when available. if ( null !== $charge ) { $payment_method_details = $charge->get_payment_method_details(); if ( $payment_method_details ) { $this->store_payment_method_details( $order, $payment_method_details ); } } } /** * Legacy version of the attach_intent_info_to_order method. * * TODO: This method should ultimately be merged with `attach_intent_info_to_order` and then removed. * * @param WC_Order $order The order. * @param string $intent_id The intent ID. * @param string $intent_status Intent status. * @param string $payment_method Payment method ID. * @param string $customer_id Customer ID. * @param string $charge_id Charge ID. * @param string $currency Currency code. * @param string $payment_transaction_id The transaction ID of the linked charge. * @param string $risk_level The risk level of the payment. * * @throws Order_Not_Found_Exception */ public function attach_intent_info_to_order__legacy( $order, $intent_id, $intent_status, $payment_method, $customer_id, $charge_id, $currency, $payment_transaction_id = null, $risk_level = null ) { // first, let's save all the metadata that needed for refunds, required for status change etc. $order->set_transaction_id( $intent_id ); $this->set_intent_id_for_order( $order, $intent_id ); $this->set_payment_method_id_for_order( $order, $payment_method ); $this->set_charge_id_for_order( $order, $charge_id ); $this->set_intention_status_for_order( $order, $intent_status ); $this->set_customer_id_for_order( $order, $customer_id ); $this->set_wcpay_intent_currency_for_order( $order, $currency ); $this->set_payment_transaction_id_for_order( $order, $payment_transaction_id ); $this->set_charge_risk_level_for_order( $order, $risk_level ); $order->save(); } /** * Create the shipping data array to send to Stripe when making a purchase. * * @param WC_Order $order The order that is being paid for. * @return array The shipping data to send to Stripe. */ public function get_shipping_data_from_order( WC_Order $order ): array { return [ 'name' => implode( ' ', array_filter( [ $order->get_shipping_first_name(), $order->get_shipping_last_name(), ] ) ), 'address' => [ 'line1' => $order->get_shipping_address_1(), 'line2' => $order->get_shipping_address_2(), 'postal_code' => $order->get_shipping_postcode(), 'city' => $order->get_shipping_city(), 'state' => $order->get_shipping_state(), 'country' => $order->get_shipping_country(), ], ]; } /** * Create the billing data array to send to Stripe when making a purchase, based on order's billing data. * It only returns the fields that are present in the billing section of the checkout. * * @param WC_Order $order The order that is being paid for. * @return array The shipping data to send to Stripe. */ public function get_billing_data_from_order( WC_Order $order ): array { $billing_fields = array_keys( WC()->countries->get_address_fields( $order->get_billing_country() ) ); $address_field_to_key = [ 'billing_city' => 'city', 'billing_country' => 'country', 'billing_address_1' => 'line1', 'billing_address_2' => 'line2', 'billing_postcode' => 'postal_code', 'billing_state' => 'state', ]; $field_to_key = [ 'billing_email' => 'email', 'billing_phone' => 'phone', ]; $billing_details = [ 'address' => [] ]; foreach ( $billing_fields as $field ) { if ( isset( $address_field_to_key[ $field ] ) ) { $billing_details['address'][ $address_field_to_key[ $field ] ] = $order->{"get_{$field}"}(); } elseif ( isset( $field_to_key[ $field ] ) ) { $billing_details[ $field_to_key[ $field ] ] = $order->{"get_{$field}"}(); } } if ( in_array( 'billing_first_name', $billing_fields, true ) && in_array( 'billing_last_name', $billing_fields, true ) ) { $billing_details['name'] = trim( $order->get_formatted_billing_full_name() ); } // The country field can't ever be empty, so we remove it if it is. if ( empty( $billing_details['address']['country'] ) ) { unset( $billing_details['address']['country'] ); } return $billing_details; } /** * Creates an "authorization cancelled" order note if not already present. * * @param WC_Order $order The order. * @param string $intent_id The ID of the intent associated with this order. * @param string $charge_id The charge ID related to the intent/order. * @return boolean True if the note was added, false otherwise. */ public function post_unique_capture_cancelled_note( $order, $intent_id, $charge_id ): bool { $note = $this->generate_capture_cancelled_note( $intent_id, $charge_id ); if ( ! $this->order_note_exists( $order, $note ) ) { $order->add_order_note( $note ); return true; } return false; } /** * Creates an "authorization captured" order note if not already present. * * @param WC_Order $order The order. * @param string $intent_id The ID of the intent associated with this order. * @param string $charge_id The charge ID related to the intent/order. * @return boolean True if the note was added, false otherwise. */ public function post_unique_capture_complete_note( $order, $intent_id, $charge_id ) { $note = $this->generate_capture_success_note( $order, $intent_id, $charge_id ); if ( ! $this->order_note_exists( $order, $note ) ) { $order->add_order_note( $note ); return true; } return false; } /** * Updates an order to cancelled status, while adding a note with a link to the transaction. * * @param WC_Order $order Order object. * @param array $intent_data The intent data associated with this order. * * @return void */ private function mark_payment_capture_cancelled( $order, $intent_data ) { if ( false === $this->post_unique_capture_cancelled_note( $order, $intent_data['intent_id'], $intent_data['charge_id'] ) ) { $this->complete_order_processing( $order ); return; } /** * If we have a status for the fraud outcome, we want to add the proper meta data. */ if ( isset( $intent_data['fraud_outcome'] ) && Rule::is_valid_fraud_outcome_status( $intent_data['fraud_outcome'] ) && Rule::FRAUD_OUTCOME_ALLOW !== $intent_data['fraud_outcome'] ) { $this->set_fraud_outcome_status_for_order( $order, $intent_data['fraud_outcome'] ); $this->set_fraud_meta_box_type_for_order( $order, Fraud_Meta_Box_Type::REVIEW_BLOCKED ); } // Remove transaction fee since the authorization was canceled and no payment was processed. $order->delete_meta_data( self::WCPAY_TRANSACTION_FEE_META_KEY ); $order->delete_meta_data( '_wcpay_net' ); $this->update_order_status( $order, Order_Status::CANCELLED ); $this->complete_order_processing( $order, $intent_data['intent_status'] ); } /** * Updates an order to processing/completed status, while adding a note with a link to the transaction. * * @param WC_Order $order Order object. * @param array $intent_data The data of the intent associated with this order. * * @return void */ private function mark_payment_completed( $order, $intent_data ) { $note = $this->generate_payment_success_note( $intent_data['intent_id'], $intent_data['charge_id'], $this->get_order_amount( $order ) ); if ( $this->order_note_exists( $order, $note ) ) { return; } // Check if a capture note already exists for this payment intent. // This prevents adding a duplicate "charged" note when the payment was already // processed via manual capture (race condition between capture flow and webhooks). $capture_note = $this->generate_capture_success_note( $order, $intent_data['intent_id'], $intent_data['charge_id'] ); if ( $this->order_note_exists( $order, $capture_note ) ) { return; } // Update the note with the fee breakdown details async, update order status, add order note. $this->enqueue_add_fee_breakdown_to_order_notes( $order, $intent_data['intent_id'] ); /** * If we have a status for the fraud outcome, we want to add the proper meta data. * If auth/capture is enabled and the transaction is allowed, it will be 'allow'. * If it was held for review for any reason, it will be 'review'. */ if ( '' !== $intent_data['fraud_outcome'] && Rule::is_valid_fraud_outcome_status( $intent_data['fraud_outcome'] ) ) { $fraud_meta_box_type = Order_Status::ON_HOLD === $order->get_status() ? Fraud_Meta_Box_Type::REVIEW_ALLOWED : Fraud_Meta_Box_Type::ALLOW; $this->set_fraud_outcome_status_for_order( $order, $intent_data['fraud_outcome'] ); $this->set_fraud_meta_box_type_for_order( $order, $fraud_meta_box_type ); } if ( ! $this->intent_has_card_payment_type( $intent_data ) ) { $this->set_fraud_meta_box_type_for_order( $order, Fraud_Meta_Box_Type::NOT_CARD ); } $this->update_order_status( $order, 'payment_complete', $intent_data['intent_id'] ); $order->add_order_note( $note ); $this->set_intention_status_for_order( $order, $intent_data['intent_status'] ); } /** * Updates an order to on-hold status, while adding a note with a link to the transaction. * * @param WC_Order $order Order object. * @param array $intent_data The intent data associated with this order. * * @return void */ private function mark_payment_authorized( $order, $intent_data ) { $note = $this->generate_payment_authorized_note( $order, $intent_data['intent_id'], $intent_data['charge_id'] ); if ( $this->order_note_exists( $order, $note ) || $order->has_status( [ Order_Status::ON_HOLD ] ) ) { return; } if ( Rule::FRAUD_OUTCOME_ALLOW === $intent_data['fraud_outcome'] ) { $this->set_fraud_outcome_status_for_order( $order, Rule::FRAUD_OUTCOME_ALLOW ); $this->set_fraud_meta_box_type_for_order( $order, Fraud_Meta_Box_Type::ALLOW ); } $this->update_order_status( $order, Order_Status::ON_HOLD ); $order->add_order_note( $note ); $this->set_intention_status_for_order( $order, $intent_data['intent_status'] ); } /** * Updates an order to on-hold status, while adding a note with a link to the transaction. * * @param WC_Order $order Order object. * @param array $intent_data The intent data associated with this order. * * @return void */ private function mark_payment_on_hold( $order, $intent_data ) { $note = $this->generate_payment_started_note( $order, $intent_data['intent_id'] ); if ( $this->order_note_exists( $order, $note ) ) { return; } $fraud_meta_box_type = $this->intent_has_card_payment_type( $intent_data ) ? Fraud_Meta_Box_Type::PAYMENT_STARTED : Fraud_Meta_Box_Type::NOT_CARD; $this->set_fraud_meta_box_type_for_order( $order, $fraud_meta_box_type ); $this->update_order_status( $order, Order_Status::ON_HOLD ); $order->add_order_note( $note ); $this->set_intention_status_for_order( $order, $intent_data['intent_status'] ); } /** * Updates an order to processing/completed status, while adding a note with a link to the transaction. * * @param WC_Order $order Order object. * @param WC_Payments_API_Payment_Intention $intent The intent instance. * * @return void */ private function mark_payment_capture_completed( $order, $intent ) { $intent_id = $intent->get_id(); $note = $this->generate_capture_success_note( $order, $intent_id, $intent->get_charge()->get_id() ); if ( $this->order_note_exists( $order, $note ) ) { return; } // Update the note with the fee breakdown details async. $this->enqueue_add_fee_breakdown_to_order_notes( $order, $intent_id ); /** * If we have a status for the fraud outcome, we want to add the proper meta data. * If auth/capture is enabled and the transaction is allowed, it will be 'allow'. * If it was held for review for any reason, it will be 'review'. */ $fraud_outcome = $intent->get_metadata()['fraud_outcome'] ?? ''; if ( '' !== $fraud_outcome && Rule::is_valid_fraud_outcome_status( $fraud_outcome ) ) { $fraud_meta_box_type = Rule::FRAUD_OUTCOME_REVIEW === $this->get_fraud_outcome_status_for_order( $order ) ? Fraud_Meta_Box_Type::REVIEW_ALLOWED : Fraud_Meta_Box_Type::ALLOW; $this->set_fraud_outcome_status_for_order( $order, $fraud_outcome ); $this->set_fraud_meta_box_type_for_order( $order, $fraud_meta_box_type ); } $this->attach_transaction_fee_to_order( $order, $intent->get_charge() ); $this->update_order_status( $order, 'payment_complete', $intent_id ); $order->add_order_note( $note ); $this->set_intention_status_for_order( $order, $intent->get_status() ); } /** * Leaves an order in pending status, while adding a note with a link to the transaction. * * @param WC_Order $order Order object. * @param array $intent_data The intent data associated with this order. * * @return void */ private function mark_payment_started( $order, $intent_data ) { $note = $this->generate_payment_started_note( $order, $intent_data['intent_id'] ); if ( $this->order_note_exists( $order, $note ) || ! $order->has_status( [ Order_Status::PENDING ] ) ) { return; } $fraud_meta_box_type = $this->intent_has_card_payment_type( $intent_data ) ? Fraud_Meta_Box_Type::PAYMENT_STARTED : Fraud_Meta_Box_Type::NOT_CARD; $this->set_fraud_meta_box_type_for_order( $order, $fraud_meta_box_type ); $order->add_order_note( $note ); $this->set_intention_status_for_order( $order, $intent_data['intent_status'] ); } /** * Changes status to On-Hold, adds fraud meta data, and adds the fraud held for review note. * * @param WC_Order $order Order object. * @param array $intent_data The intent data associated with this order. * * @return void */ private function mark_order_held_for_review_for_fraud( $order, $intent_data ) { $note = $this->generate_fraud_held_for_review_note( $order, $intent_data['intent_id'], $intent_data['charge_id'] ); if ( $this->order_note_exists( $order, $note ) ) { return; } $this->update_order_status( $order, Order_Status::ON_HOLD ); $this->set_fraud_outcome_status_for_order( $order, Rule::FRAUD_OUTCOME_REVIEW ); $this->set_fraud_meta_box_type_for_order( $order, Fraud_Meta_Box_Type::REVIEW ); $order->add_order_note( $note ); $this->set_intention_status_for_order( $order, $intent_data['intent_status'] ); } /** * Given the charge, adds the application_fee_amount from the charge to the given order as metadata. * * @param WC_Order $order The order to update. * @param WC_Payments_API_Charge|null $charge The charge to get the application_fee_amount from. */ public function attach_transaction_fee_to_order( $order, $charge ) { try { // Only set transaction fee if the charge was actually captured. // Canceled authorizations should not have fees since no payment was processed. if ( $charge && null !== $charge->get_application_fee_amount() && $charge->is_captured() ) { $order->update_meta_data( self::WCPAY_TRANSACTION_FEE_META_KEY, WC_Payments_Utils::interpret_stripe_amount( $charge->get_application_fee_amount(), $charge->get_currency() ) ); $order->save_meta_data(); } } catch ( Exception $e ) { // Log the error and don't block checkout. Logger::log( 'Error saving transaction fee into metadata for the order ' . $order->get_id() . ': ' . $e->getMessage() ); } } /** * Cancels uncaptured authorizations on order cancel. * * @param int $order_id - Order ID. */ public function cancel_authorizations_on_order_status_change( $order_id ) { $order = new WC_Order( $order_id ); if ( null !== $order ) { $intent_id = $this->get_intent_id_for_order( $order ); if ( null !== $intent_id && '' !== $intent_id ) { try { $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); $intent = $request->send(); $charge = $intent->get_charge(); /** * Successful but not captured Charge is an authorization * that needs to be cancelled. */ if ( null !== $charge && false === $charge->is_captured() && Intent_Status::SUCCEEDED === $charge->get_status() && Intent_Status::REQUIRES_CAPTURE === $intent->get_status() ) { $request = Cancel_Intention::create( $intent_id ); $request->set_hook_args( $order ); $intent = $request->send(); $this->post_unique_capture_cancelled_note( $order, $intent_id, $charge->get_id() ); // Remove transaction fee since the authorization was canceled and no payment was processed. $order->delete_meta_data( self::WCPAY_TRANSACTION_FEE_META_KEY ); $order->delete_meta_data( '_wcpay_net' ); } $this->set_intention_status_for_order( $order, $intent->get_status() ); $order->save(); } catch ( \Exception $e ) { $order->add_order_note( WC_Payments_Utils::esc_interpolated_html( __( 'Canceling authorization <strong>failed</strong> to complete.', 'woocommerce-payments' ), [ 'strong' => '<strong>' ] ) ); } } } } /** * Handles the change of an order status. * * This function is triggered when the status of an order is changed. * It performs necessary actions based on the new status of the order. * * @param int $order_id The ID of the order. * @return void */ public function capture_authorization_on_order_status_change( int $order_id ) { $order = new WC_Order( $order_id ); if ( null !== $order ) { $intent_id = $this->get_intent_id_for_order( $order ); if ( null !== $intent_id && '' !== $intent_id ) { try { $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); $intent = $request->send(); $charge = $intent->get_charge(); /** * Successful but not captured Charge is an authorization * that needs to be captured. */ if ( null !== $charge && false === $charge->is_captured() && Intent_Status::SUCCEEDED === $charge->get_status() && Intent_Status::REQUIRES_CAPTURE === $intent->get_status() ) { $request = Capture_Intention::create( $intent_id ); $request->set_amount_to_capture( WC_Payments_Utils::prepare_amount( $order->get_total(), $order->get_currency() ) ); $request->set_hook_args( $order ); $intent = $request->send(); $this->post_unique_capture_complete_note( $order, $intent_id, $charge->get_id() ); $this->enqueue_add_fee_breakdown_to_order_notes( $order, $intent_id ); } $this->set_intention_status_for_order( $order, $intent->get_status() ); $order->save(); } catch ( \Exception $e ) { $order->add_order_note( WC_Payments_Utils::esc_interpolated_html( __( 'Capture authorization <strong>failed</strong> to complete.', 'woocommerce-payments' ), [ 'strong' => '<strong>' ] ) ); } } } } /** * Creates a refund for the given order. * * @param WC_Order $order The order to refund. * @param float $amount The amount to refund. * @param string $reason The reason for the refund. * @param array $line_items The line items to refund. * * @throws Exception If the refund creation fails. */ public function create_refund_for_order( WC_Order $order, float $amount, string $reason = '', array $line_items = [] ) { $refund_params = [ 'amount' => wc_format_decimal( $amount, wc_get_price_decimals() ), 'reason' => $reason, 'order_id' => $order->get_id(), ]; if ( $line_items ) { $refund_params['line_items'] = $line_items; } $refund = wc_create_refund( $refund_params ); if ( is_wp_error( $refund ) ) { throw new Exception( esc_html( $refund->get_error_message() ) ); } return $refund; } /** * Adds a note and metadata for a refund. * * @param WC_Order $order The order to refund. * @param WC_Order_Refund $wc_refund The WC refund object. * @param string $refund_id The refund ID. * @param string|null $refund_balance_transaction_id The balance transaction ID of the refund. * @param bool $is_pending Created refund status can be either pending or succeeded. Default false, i.e. succeeded. * @throws Order_Not_Found_Exception * @throws Exception */ public function add_note_and_metadata_for_created_refund( WC_Order $order, WC_Order_Refund $wc_refund, string $refund_id, ?string $refund_balance_transaction_id, bool $is_pending = false ): void { $note = $this->generate_payment_created_refund_note( $wc_refund->get_amount(), $wc_refund->get_currency(), $refund_id, $wc_refund->get_reason(), $order, $is_pending ); if ( ! $this->order_note_exists( $order, $note ) ) { $order->add_order_note( $note ); } // Use `successful` to maintain the backward compatibility with the previous WooPayments versions. $this->set_wcpay_refund_status_for_order( $order, $is_pending ? Refund_Status::PENDING : 'successful' ); $this->set_wcpay_refund_id_for_refund( $wc_refund, $refund_id ); if ( isset( $refund_balance_transaction_id ) ) { $this->set_wcpay_refund_transaction_id_for_order( $wc_refund, $refund_balance_transaction_id ); } $order->save(); } /** * Handle a failed refund by adding a note, updating metadata, and optionally deleting the refund. * * @param WC_Order $order The order to add the note to. * @param string $refund_id The ID of the failed refund. * @param int $amount The refund amount in cents. * @param string $currency The currency code. * @param WC_Order_Refund|null $wc_refund The WC refund object to delete if provided. * @param bool $is_cancelled Whether this is a cancellation rather than a failure. Default false. * @param string|null $failure_reason The reason for the refund failure. Default null. * @return void */ public function handle_failed_refund( WC_Order $order, string $refund_id, int $amount, string $currency, ?WC_Order_Refund $wc_refund = null, bool $is_cancelled = false, ?string $failure_reason = null ): void { // Delete the refund if it exists. if ( $wc_refund ) { $wc_refund->delete(); } $formatted_amount = WC_Payments_Explicit_Price_Formatter::get_explicit_price( wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount, $currency ), [ 'currency' => strtoupper( $currency ) ] ), $order ); // Handle insufficient balance case first to avoid duplicate notes. if ( Refund_Failure_Reason::INSUFFICIENT_FUNDS === $failure_reason ) { $this->handle_insufficient_balance_for_refund( $order, $amount ); } else { $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1$s: the refund amount, %2$s: status (cancelled/unsuccessful), %3$s: WooPayments, %4$s: ID of the refund, %5$s: failure message or period */ __( 'A refund of %1$s was <strong>%2$s</strong> using %3$s (<code>%4$s</code>)%5$s', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'code' => '<code>', ] ), $formatted_amount, $is_cancelled ? __( 'cancelled', 'woocommerce-payments' ) : __( 'unsuccessful', 'woocommerce-payments' ), 'WooPayments', $refund_id, $is_cancelled ? '.' : ': ' . Refund_Failure_Reason::get_failure_message( $failure_reason ?? Refund_Failure_Reason::UNKNOWN ), ); if ( $this->order_note_exists( $order, $note ) ) { return; } $order->add_order_note( $note ); } // If order has been fully refunded, change status to failed. if ( Order_Status::REFUNDED === $order->get_status() ) { $order->update_status( Order_Status::FAILED ); } $this->set_wcpay_refund_status_for_order( $order, Refund_Status::FAILED ); $order->save(); } /** * Get content for the success order note. * * @param string $intent_id The payment intent ID related to the intent/order. * @param string $charge_id The charge ID related to the intent/order. * @param string $formatted_amount The formatted order total. * * @return string Note content. */ private function generate_payment_success_note( $intent_id, $charge_id, $formatted_amount ) { $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); return sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the successfully charged amount, %2: WooPayments, %3: transaction ID of the payment */ __( 'A payment of %1$s was <strong>successfully charged</strong> using %2$s (<a>%3$s</a>).', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'a' => ! empty( $transaction_url ) ? '<a href="' . $transaction_url . '" target="_blank" rel="noopener noreferrer">' : '<code>', ] ), $formatted_amount, 'WooPayments', WC_Payments_Utils::get_transaction_url_id( $intent_id, $charge_id ) ); } /** * Get content for the failure order note and additional message, if included. * * @param string $intent_id The ID of the intent associated with this order. * @param string $charge_id The charge ID related to the intent/order. * @param string $message Optional message to add to the note. * @param string $formatted_amount The formatted order total. * * @return string Note content. */ private function generate_payment_failure_note( $intent_id, $charge_id, $message, $formatted_amount ) { $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the authorized amount, %2: WooPayments, %3: transaction ID of the payment */ __( 'A payment of %1$s <strong>failed</strong> using %2$s (<a>%3$s</a>).', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'a' => ! empty( $transaction_url ) ? '<a href="' . $transaction_url . '" target="_blank" rel="noopener noreferrer">' : '<code>', ] ), $formatted_amount, 'WooPayments', WC_Payments_Utils::get_transaction_url_id( $intent_id, $charge_id ) ); if ( ! empty( $message ) ) { $note .= ' ' . $message; } return $note; } /** * Get content for the failure order note and additional message, if included. * * @param string $intent_id The ID of the intent associated with this order. * @param string $charge_id The charge ID related to the intent/order. * @param string $message Optional message to add to the note. * @param string $formatted_amount The formatted order total. * * @return string Note content. */ private function generate_terminal_payment_failure_note( $intent_id, $charge_id, $message, $formatted_amount ) { // Add charge_id to the transaction URL instead of intent_id for uniqueness. $transaction_url = WC_Payments_Utils::compose_transaction_url( '', $charge_id ); $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the authorized amount, %2: WooPayments, %3: transaction ID of the payment, %4: timestamp */ __( 'A terminal payment of %1$s <strong>failed</strong> using %2$s (<a>%3$s</a>)', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'a' => ! empty( $transaction_url ) ? '<a href="' . $transaction_url . '" target="_blank" rel="noopener noreferrer">' : '<code>', ] ), $formatted_amount, 'WooPayments', $intent_id ?? $charge_id ); if ( ! empty( $message ) ) { $note .= ' ' . $message; } return $note; } /** * Generates the payment authorized order note. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * @param string $charge_id The charge ID related to the intent/order. * * @return string */ private function generate_payment_authorized_note( $order, $intent_id, $charge_id ): string { $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the authorized amount, %2: WooPayments, %3: transaction ID of the payment */ __( 'A payment of %1$s was <strong>authorized</strong> using %2$s (<a>%3$s</a>).', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'a' => ! empty( $transaction_url ) ? '<a href="' . $transaction_url . '" target="_blank" rel="noopener noreferrer">' : '<code>', ] ), $this->get_order_amount( $order ), 'WooPayments', WC_Payments_Utils::get_transaction_url_id( $intent_id, $charge_id ) ); return $note; } /** * Generates the payment started order note. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * * @return string */ private function generate_payment_started_note( $order, $intent_id ): string { $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the authorized amount, %2: WooPayments, %3: intent ID of the payment */ __( 'A payment of %1$s was <strong>started</strong> using %2$s (<code>%3$s</code>).', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'code' => '<code>', ] ), $this->get_order_amount( $order ), 'WooPayments', $intent_id ); return $note; } /** * Generates the successful capture order note. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * @param string $charge_id The charge ID related to the intent/order. * * @return string */ private function generate_capture_success_note( $order, $intent_id, $charge_id ) { $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the successfully charged amount, %2: WooPayments, %3: transaction ID of the payment */ __( 'A payment of %1$s was <strong>successfully captured</strong> using %2$s (<a>%3$s</a>).', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'a' => ! empty( $transaction_url ) ? '<a href="' . $transaction_url . '" target="_blank" rel="noopener noreferrer">' : '<code>', ] ), $this->get_order_amount( $order ), 'WooPayments', WC_Payments_Utils::get_transaction_url_id( $intent_id, $charge_id ) ); return $note; } /** * Generates the failure order note and additional message, if included. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * @param string $charge_id The charge ID related to the intent/order. * @param string $message Optional message to add to the note. * * @return string */ private function generate_capture_failed_note( $order, $intent_id, $charge_id, $message ): string { $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the authorized amount, %2: WooPayments, %3: transaction ID of the payment */ __( 'A capture of %1$s <strong>failed</strong> to complete using %2$s (<a>%3$s</a>).', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'a' => ! empty( $transaction_url ) ? '<a href="' . $transaction_url . '" target="_blank" rel="noopener noreferrer">' : '<code>', ] ), $this->get_order_amount( $order ), 'WooPayments', WC_Payments_Utils::get_transaction_url_id( $intent_id, $charge_id ) ); if ( ! empty( $message ) ) { $note .= ' ' . $message; } return $note; } /** * Get content for the capture expired note. * * @param string $intent_id The ID of the intent associated with this order. * @param string $charge_id The charge ID related to the intent/order. * * @return string Note content. */ private function generate_capture_expired_note( $intent_id, $charge_id ) { $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); return sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the authorized amount, %2: transaction ID of the payment */ __( 'Payment authorization has <strong>expired</strong> (<a>%1$s</a>).', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'a' => ! empty( $transaction_url ) ? '<a href="' . $transaction_url . '" target="_blank" rel="noopener noreferrer">' : '<code>', ] ), WC_Payments_Utils::get_transaction_url_id( $intent_id, $charge_id ) ); } /** * Generates the capture cancelled order note. * * @param string $intent_id The ID of the intent associated with this order. * @param string $charge_id The charge ID related to the intent/order. * * @return string */ private function generate_capture_cancelled_note( $intent_id, $charge_id ): string { $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); return sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: transaction ID of the payment */ __( 'Payment authorization was successfully <strong>cancelled</strong> (<a>%1$s</a>).', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'a' => ! empty( $transaction_url ) ? '<a href="' . $transaction_url . '" target="_blank" rel="noopener noreferrer">' : '<code>', ] ), WC_Payments_Utils::get_transaction_url_id( $intent_id, $charge_id ) ); } /** * Generates the fraud held for review order note. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * @param string $charge_id The charge ID related to the intent/order. * * @return string */ private function generate_fraud_held_for_review_note( $order, $intent_id, $charge_id ): string { $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id, [ 'status_is' => Rule::FRAUD_OUTCOME_REVIEW, 'type_is' => 'order_note', ] ); $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the authorized amount, %2: transaction ID of the payment */ __( '⛔ A payment of %1$s was <strong>held for review</strong> by one or more risk filters.<br><br><a>View more details</a>.', 'woocommerce-payments' ), [ '⛔' => '⛔', 'strong' => '<strong>', 'br' => '<br>', 'a' => ! empty( $transaction_url ) ? '<a href="' . $transaction_url . '" target="_blank" rel="noopener noreferrer">' : '<code>', ] ), $this->get_order_amount( $order ) ); return $note; } /** * Generates the fraud blocked order note. * * @param WC_Order $order Order object. * * @return string */ private function generate_fraud_blocked_note( $order ): string { $transaction_url = WC_Payments_Utils::compose_transaction_url( $order->get_id(), '', [ 'status_is' => Rule::FRAUD_OUTCOME_BLOCK, 'type_is' => 'order_note', ] ); $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the blocked amount, %2: transaction ID of the payment */ __( '🚫 A payment of %1$s was <strong>blocked</strong> by one or more risk filters.<br><br><a>View more details</a>.', 'woocommerce-payments' ), [ '🚫' => '🚫', 'strong' => '<strong>', 'br' => '<br>', 'a' => ! empty( $transaction_url ) ? '<a href="' . $transaction_url . '" target="_blank" rel="noopener noreferrer">' : '<code>', ] ), $this->get_order_amount( $order ) ); return $note; } /** * Get content for the dispute created order note. * * @param string $charge_id The ID of the disputes charge associated with this order. * @param string $amount The disputed amount – formatted currency value. * @param string $reason The reason for the dispute – human-readable text. * @param string $due_by The deadline for responding to the dispute - formatted date string. * @param bool $is_inquiry Whether the dispute is an inquiry or not. * * @return string Note content. */ private function generate_dispute_created_note( $charge_id, $amount, $reason, $due_by, $is_inquiry = false ) { $dispute_url = $this->compose_dispute_url( $charge_id ); // Get merchant-friendly dispute reason description. $reason = WC_Payments_Utils::get_dispute_reason_description( $reason ); if ( $is_inquiry ) { return sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the disputed amount and currency; %2: the dispute reason; %3 the deadline date for responding to the inquiry */ __( 'A payment inquiry has been raised for %1$s with reason "%2$s". <a>Response due by %3$s</a>.', 'woocommerce-payments' ), [ 'a' => '<a href="%4$s" target="_blank" rel="noopener noreferrer">', ] ), $amount, $reason, $due_by, $dispute_url ); } return sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the disputed amount and currency; %2: the dispute reason; %3 the deadline date for responding to dispute */ __( 'Payment has been disputed for %1$s with reason "%2$s". <a>Response due by %3$s</a>.', 'woocommerce-payments' ), [ 'a' => '<a href="%4$s" target="_blank" rel="noopener noreferrer">', ] ), $amount, $reason, $due_by, $dispute_url ); } /** * Get content for the dispute closed order note. * * @param string $charge_id The ID of the disputed charge associated with this order. * @param string $status The status of the dispute. * @param bool $is_inquiry Whether the dispute is an inquiry or not. * * @return string Note content. */ private function generate_dispute_closed_note( $charge_id, $status, $is_inquiry = false ) { $dispute_url = $this->compose_dispute_url( $charge_id ); if ( $is_inquiry ) { return sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the dispute status */ __( 'Payment inquiry has been closed with status %1$s. See <a>payment status</a> for more details.', 'woocommerce-payments' ), [ 'a' => '<a href="%2$s" target="_blank" rel="noopener noreferrer">', ] ), $status, $dispute_url ); } return sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the dispute status */ __( 'Dispute has been closed with status %1$s. See <a>dispute overview</a> for more details.', 'woocommerce-payments' ), [ 'a' => '<a href="%2$s" target="_blank" rel="noopener noreferrer">', ] ), $status, $dispute_url ); } /** * Generates the HTML note for a refunded payment. * * @param float $refunded_amount Amount refunded. * @param string $refunded_currency Refund currency. * @param string $wcpay_refund_id WCPay Refund ID. * @param string $refund_reason Refund reason. * @param WC_Order $order Order object. * @param bool $is_pending Created refund status can be either pending or succeeded. Default false, i.e. succeeded. * * @return string HTML note. */ private function generate_payment_created_refund_note( float $refunded_amount, string $refunded_currency, string $wcpay_refund_id, string $refund_reason, WC_Order $order, bool $is_pending ): string { $multi_currency_instance = WC_Payments_Multi_Currency(); $formatted_price = WC_Payments_Explicit_Price_Formatter::get_explicit_price( $multi_currency_instance->get_backend_formatted_wc_price( $refunded_amount, [ 'currency' => strtoupper( $refunded_currency ) ] ), $order ); $status_text = $is_pending ? sprintf( '<a href="https://woocommerce.com/document/woopayments/managing-money/#pending-refunds" target="_blank" rel="noopener noreferrer">%1$s</a>', __( 'is pending', 'woocommerce-payments' ) ) : __( 'was successfully processed', 'woocommerce-payments' ); if ( empty( $refund_reason ) ) { $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the refund amount, %2: WooPayments, %3: ID of the refund, %4: status text */ __( 'A refund of %1$s %4$s using %2$s (<code>%3$s</code>).', 'woocommerce-payments' ), [ 'code' => '<code>', ] ), $formatted_price, 'WooPayments', $wcpay_refund_id, $status_text ); } else { $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the refund amount, %2: WooPayments, %3: reason, %4: refund id, %5: status text */ __( 'A refund of %1$s %5$s using %2$s. Reason: %3$s. (<code>%4$s</code>)', 'woocommerce-payments' ), [ 'code' => '<code>', ] ), $formatted_price, 'WooPayments', $refund_reason, $wcpay_refund_id, $status_text ); } return $note; } /** * Composes url for dispute details page. * * @param string $charge_id The disputed charge ID. * * @return string Transaction details page url. */ private function compose_dispute_url( $charge_id ) { return add_query_arg( [ 'page' => 'wc-admin', 'path' => rawurlencode( '/payments/transactions/details' ), 'id' => $charge_id, ], admin_url( 'admin.php' ) ); } /** * Check if order is locked for payment processing * * @param WC_Order $order The order that is being paid. * @param string $intent_id The id of the intent that is being processed. * * @return bool A flag that indicates whether the order is already locked. */ private function is_order_locked( $order, $intent_id = null ) { $order_id = $order->get_id(); $transient_name = 'wcpay_processing_intent_' . $order_id; $processing = get_transient( $transient_name ); // Block the process if the same intent is already being handled. return ( '-1' === $processing || ( isset( $intent_id ) && $processing === $intent_id ) ); } /** * Lock an order for payment intent processing for 5 minutes. * * @param WC_Order $order The order that is being paid. * @param string $intent_id The id of the intent that is being processed. * * @return void */ private function lock_order_payment( $order, $intent_id = null ) { $order_id = $order->get_id(); $transient_name = 'wcpay_processing_intent_' . $order_id; set_transient( $transient_name, empty( $intent_id ) ? '-1' : $intent_id, 5 * MINUTE_IN_SECONDS ); } /** * Unlocks an order for processing by payment intents. * * @param WC_Order $order The order that is being unlocked. * * @return void */ private function unlock_order_payment( $order ) { $order_id = $order->get_id(); delete_transient( 'wcpay_processing_intent_' . $order_id ); } /** * Refreshes the order from the database, checks if it is locked, and locks it. * * TODO: Update to throw exceptions so try/catch can be used. * TODO: Maybe add checks to see if there is already a successful intent, or the intent status passed is already set. * * @param WC_Order $order Order object. * @param string $intent_id The ID of the intent associated with this order. * * @return bool */ private function order_prepared_for_processing( $order, $intent_id ) { if ( ! is_a( $order, 'WC_Order' ) ) { return false; } if ( $this->is_order_paid( $order ) ) { return false; } if ( $this->is_order_locked( $order, $intent_id ) ) { return false; } // Lock the order. $this->lock_order_payment( $order, $intent_id ); return true; } /** * Checks to see if the current order, and a fresh copy of the order from the database are paid. * * @param WC_Order $order The order being checked. * * @return boolean True if it has a paid status, false if not. */ private function is_order_paid( $order ) { wp_cache_delete( $order->get_id(), 'posts' ); // Read the latest order properties from the database to avoid race conditions if webhook was handled during this request. $clone_order = clone $order; $clone_order->get_data_store()->read( $clone_order ); // Check if the order is already complete. if ( function_exists( 'wc_get_is_paid_statuses' ) ) { if ( $order->has_status( wc_get_is_paid_statuses() ) || $clone_order->has_status( wc_get_is_paid_statuses() ) ) { return true; } } return false; } /** * Completes order processing by updating the intent meta, unlocking the order, and saving the order. * * @param WC_Order $order Order object. * @param string|null $intent_status The status of the intent related to this order. * * @return void */ private function complete_order_processing( $order, $intent_status = null ) { if ( ! empty( $intent_status ) ) { $this->set_intention_status_for_order( $order, $intent_status ); } $this->unlock_order_payment( $order ); $order->save(); } /** * Gets the total for the order in explicit format. * * @param WC_Order $order Order object. * * @return string The formatted order total. */ private function get_order_amount( $order ) { $multi_currency_instance = WC_Payments_Multi_Currency(); $order_price = $order->get_total(); $formatted_price = $multi_currency_instance->get_backend_formatted_wc_price( $order_price, [ 'currency' => $order->get_currency() ] ); return WC_Payments_Explicit_Price_Formatter::get_explicit_price( $formatted_price, $order ); } /** * Updates the order status and catches any exceptions so that processing can continue. * * @param WC_Order $order Order object. * @param string $order_status The status to change the order to. * @param null|string $intent_id The ID of the intent associated with this order. * * @throws Exception Throws exception if intent id is not included if order needs to be marked as paid. * * @return void */ private function update_order_status( $order, $order_status, $intent_id = '' ) { try { /** * In this instance payment_complete is not an order status, but a flag to mark the order as paid. In a default WooCommerce store, the order * may move to Processing or Completed status depending on the contents of the cart, so we let WooCommerce core decide what to do. */ if ( 'payment_complete' === $order_status ) { if ( empty( $intent_id ) ) { throw new Exception( __( 'Intent id was not included for payment complete status change.', 'woocommerce-payments' ) ); } $order->payment_complete( $intent_id ); } else { $order->update_status( $order_status ); } } catch ( Exception $e ) { // Continue further, something unexpected happened, but we can't really do anything with that. Logger::log( 'Error when updating status for order ' . $order->get_id() . ': ' . $e->getMessage() ); } } /** * Takes an intent object or array and returns our needed data as an array. * This is needed due to intents can either be objects or arrays. * * @param WC_Payments_API_Abstract_Intention $intent Setup or payment intent to pull the data from. * * @return array The data we need to continue processing. */ private function get_intent_data( WC_Payments_API_Abstract_Intention $intent ): array { $intent_data = [ 'intent_id' => $intent->get_id(), 'intent_status' => $intent->get_status(), 'charge_id' => '', 'fraud_outcome' => $intent->get_metadata()['fraud_outcome'] ?? '', 'payment_method_type' => $intent->get_payment_method_type(), ]; if ( $intent instanceof WC_Payments_API_Payment_Intention ) { $charge = $intent->get_charge(); $intent_data['charge_id'] = $charge ? $charge->get_id() : null; $intent_data['error'] = $intent->get_last_payment_error(); } return $intent_data; } /** * Schedules an action to add the fee breakdown to order notes. * * @param WC_Order $order The order to add the note to. * @param string $intent_id The intent ID for the order. * * @return void */ private function enqueue_add_fee_breakdown_to_order_notes( WC_Order $order, string $intent_id ) { WC_Payments::get_action_scheduler_service()->schedule_job( time(), self::ADD_FEE_BREAKDOWN_TO_ORDER_NOTES, [ 'order_id' => $order->get_id(), 'intent_id' => $intent_id, 'is_test_mode' => WC_Payments::mode()->is_test(), ] ); } /** * If an order object is passed in, return that, else try to get the order. * This is needed due to mocked orders cannot be retrieved from the database in tests. * * @param mixed $order The order to be returned. * * @return WC_Order|WC_Order_Refund * * @throws Order_Not_Found_Exception */ private function get_order( $order ) { $order = $this->is_order_type_object( $order ) ? $order : wc_get_order( $order ); if ( ! $this->is_order_type_object( $order ) ) { throw new Order_Not_Found_Exception( esc_html__( 'The requested order was not found.', 'woocommerce-payments' ), 'order_not_found' ); } return $order; } /** * Checks to see if the given argument is an order type object. * * @param mixed $order The order to be checked. * * @return bool */ private function is_order_type_object( $order ): bool { if ( is_a( $order, 'WC_Order' ) || is_a( $order, 'WC_Order_Refund' ) ) { return true; } return false; } /** * Checks to see if the intent data has just card set as the payment method type. * * @param array $intent_data The intent data obtained from get_intent_data. * * @return bool */ private function intent_has_card_payment_type( $intent_data ): bool { return isset( $intent_data['payment_method_type'] ) && 'card' === $intent_data['payment_method_type']; } /** * Countries where FROD balance is not supported. * * @var array */ const FROD_UNSUPPORTED_COUNTRIES = [ 'HK', 'SG', 'AE' ]; /** * Handle insufficient balance for refund. * * @param WC_Order $order The order being refunded. * @param int $stripe_amount The refund amount. */ public function handle_insufficient_balance_for_refund( WC_Order $order, int $stripe_amount ) { $account_country = WC_Payments::get_account_service()->get_account_country(); $formatted_amount = wc_price( WC_Payments_Utils::interpret_stripe_amount( $stripe_amount, $order->get_currency() ), [ 'currency' => $order->get_currency() ] ); if ( $this->is_frod_supported( $account_country ) ) { $order->add_order_note( $this->get_frod_support_note( $formatted_amount ) ); } else { $order->add_order_note( $this->get_insufficient_balance_note( $formatted_amount ) ); } } /** * Attach Multibanco information to the order. * * @param WC_Order $order The order being paid. * @param string $reference The Multibanco reference. * @param string $entity The Multibanco entity. * @param string $url The Multibanco URL. * @param int $expiry The Multibanco expiry. */ public function attach_multibanco_info_to_order( WC_Order $order, string $reference, string $entity, string $url, int $expiry ): void { $order->update_meta_data( self::WCPAY_MULTIBANCO_REFERENCE_META_KEY, $reference ); $order->update_meta_data( self::WCPAY_MULTIBANCO_ENTITY_META_KEY, $entity ); $order->update_meta_data( self::WCPAY_MULTIBANCO_URL_META_KEY, $url ); $order->update_meta_data( self::WCPAY_MULTIBANCO_EXPIRY_META_KEY, $expiry ); } /** * Get Multibanco information from the order. * * @param WC_Order $order The order. * @return array */ public function get_multibanco_info_from_order( WC_Order $order ): array { return [ 'reference' => $order->get_meta( self::WCPAY_MULTIBANCO_REFERENCE_META_KEY ), 'entity' => $order->get_meta( self::WCPAY_MULTIBANCO_ENTITY_META_KEY ), 'url' => $order->get_meta( self::WCPAY_MULTIBANCO_URL_META_KEY ), 'expiry' => $order->get_meta( self::WCPAY_MULTIBANCO_EXPIRY_META_KEY ), ]; } /** * Store payment method details in the order meta. * * @param WC_Order $order The order. * @param array $payment_method_details The payment method details. * @return void */ public function store_payment_method_details( WC_Order $order, array $payment_method_details ): void { $order->update_meta_data( self::PAYMENT_METHOD_DETAILS_META_KEY, wp_json_encode( $payment_method_details ) ); $order->save_meta_data(); } /** * Get cached payment method details from the order meta. * * @param WC_Order $order The order. * @return array The payment method details. */ public function get_payment_method_details( WC_Order $order ): ?array { $json = $order->get_meta( self::PAYMENT_METHOD_DETAILS_META_KEY ); if ( '' === $json ) { return null; } return json_decode( $json, true ); } /** * Check if FROD is supported for the given country. * * @param string $country_code Two-letter country code. * @return bool */ private function is_frod_supported( $country_code ) { return ! in_array( $country_code, self::FROD_UNSUPPORTED_COUNTRIES, true ); } /** * Get the order note for FROD supported countries. * * @param string $formatted_amount The formatted refund amount. * @return string */ private function get_frod_support_note( $formatted_amount ) { $learn_more_url = 'https://woocommerce.com/document/woopayments/fees/preventing-negative-balances/#adding-funds'; return sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %s: Formatted refund amount */ __( 'Refund of %s <strong>failed</strong> due to insufficient funds in your WooPayments balance. To prevent delays in refunding customers, please consider adding funds to your Future Refunds or Disputes (FROD) balance. <a>Learn more</a>.', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'a' => '<a href="' . $learn_more_url . '" target="_blank" rel="noopener noreferrer">', ] ), $formatted_amount ); } /** * Get the order note for countries without FROD support. * * @param string $formatted_amount The formatted refund amount. * @return string */ private function get_insufficient_balance_note( $formatted_amount ) { return sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1$s: Formatted refund amount */ __( 'Refund of %1$s <strong>failed</strong> due to insufficient funds in your WooPayments balance.', 'woocommerce-payments' ), [ 'strong' => '<strong>', ] ), $formatted_amount ); } } includes/class-wc-payments-file-service.php 0000777 00000001173 15251156605 0015027 0 ustar 00 <?php /** * WC_Payments_File_Service class * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; /** * Class which handles files. */ class WC_Payments_File_Service { const FILE_PURPOSE_PUBLIC = [ 'business_logo', 'business_icon', ]; const CACHE_KEY_PREFIX_PURPOSE = 'file_purpoose_'; const CACHE_PERIOD = 86400; // 24 h /** * Check if a file purpose is public or needs permissions. * * @param string $purpose - file purpose. * * @return bool */ public function is_file_public( string $purpose ): bool { return in_array( $purpose, static::FILE_PURPOSE_PUBLIC, true ); } } includes/class-compatibility-service.php 0000777 00000007747 15251156605 0014531 0 ustar 00 <?php /** * Compatibility_Service class * * @package WooCommerce\Payments */ namespace WCPay; use WC_Payments; use WC_Payments_API_Client; use WCPay\Exceptions\API_Exception; defined( 'ABSPATH' ) || exit; // block direct access. /** * Class to send compatibility data to the server. */ class Compatibility_Service { const UPDATE_COMPATIBILITY_DATA = 'wcpay_update_compatibility_data'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * Constructor for Compatibility_Service. * * @param WC_Payments_API_Client $payments_api_client WooCommerce Payments API client. */ public function __construct( WC_Payments_API_Client $payments_api_client ) { $this->payments_api_client = $payments_api_client; } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_action( 'woocommerce_payments_account_refreshed', [ $this, 'update_compatibility_data' ] ); add_action( 'after_switch_theme', [ $this, 'update_compatibility_data' ] ); add_filter( 'wc_payments_get_onboarding_data_args', [ $this, 'add_compatibility_onboarding_data' ] ); } /** * Schedules the sending of the compatibility data to send only the last update in T minutes. * * @return void */ public function update_compatibility_data() { // This will delete the previous compatibility requests in the last two minutes, and only send the last update to the server, ensuring there's only one update in two minutes. WC_Payments::get_action_scheduler_service()->schedule_job( time() + 2 * MINUTE_IN_SECONDS, self::UPDATE_COMPATIBILITY_DATA ); } /** * Gets the data we need to confirm compatibility and sends it to the server. * * @return void */ public function update_compatibility_data_hook() { $this->payments_api_client->update_compatibility_data( $this->get_compatibility_data() ); } /** * Adds the compatibility data to the onboarding args. * * @param array $args The args being sent when onboarding. * * @return array */ public function add_compatibility_onboarding_data( $args ): array { $args['compatibility_data'] = $this->get_compatibility_data(); return $args; } /** * Gets the compatibility data. * * @return array */ private function get_compatibility_data(): array { $active_plugins = get_option( 'active_plugins', [] ); $post_types_count = $this->get_post_types_count(); $wc_permalinks = get_option( 'woocommerce_permalinks', [] ); $wc_shop_permalink = $this->get_permalink_for_page_id( 'shop' ); $wc_cart_permalink = $this->get_permalink_for_page_id( 'cart' ); $wc_checkout_permalink = $this->get_permalink_for_page_id( 'checkout' ); return [ 'woopayments_version' => WCPAY_VERSION_NUMBER, 'woocommerce_version' => WC_VERSION, 'woocommerce_permalinks' => $wc_permalinks, 'woocommerce_shop' => $wc_shop_permalink, 'woocommerce_cart' => $wc_cart_permalink, 'woocommerce_checkout' => $wc_checkout_permalink, 'blog_theme' => get_stylesheet(), 'active_plugins' => $active_plugins, 'post_types_count' => $post_types_count, ]; } /** * Gets the count of public posts for each post type. * * @return array<\WP_Post_Type|string, int> */ private function get_post_types_count(): array { $post_types = get_post_types( [ 'public' => true, ] ); $post_types_count = []; foreach ( $post_types as $post_type ) { $post_types_count[ $post_type ] = (int) wp_count_posts( $post_type )->publish; } return $post_types_count; } /** * Gets the permalink for a page ID. * * @param string $page_id The page ID to get the permalink for. * * @return string The permalink for the page ID, or 'Not set' if the permalink is not available. */ private function get_permalink_for_page_id( string $page_id ): string { $permalink = get_permalink( wc_get_page_id( $page_id ) ); return $permalink ? $permalink : 'Not set'; } } includes/migrations/class-erase-deprecated-flags-and-options.php 0000777 00000005027 15251156605 0021103 0 ustar 00 <?php /** * Class Erase_Deprecated_Flags_And_Options * * @package WooCommerce\Payments */ namespace WCPay\Migrations; defined( 'ABSPATH' ) || exit; /** * Class Erase_Deprecated_Flags_And_Options * * Clearing up all flags and options that are no longer in use. * * @since 8.1.0 */ class Erase_Deprecated_Flags_And_Options { /** * Checks whether it's worth doing the migration. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); // feel free to modify the version here to the next one, if you add a new flag to be deleted in the `migrate` method. if ( version_compare( '9.8.0', $previous_version, '>' ) ) { $this->migrate(); } } /** * Does the actual migration. */ private function migrate() { // please update as necessary, when feature flags are removed. // you don't need to check for previous values or if the migration already ran in previous versions. // if the migration did already run, the operations below will just be noop. delete_option( '_wcpay_feature_auth_and_capture' ); delete_option( '_wcpay_feature_progressive_onboarding' ); delete_option( '_wcpay_feature_client_secret_encryption' ); delete_option( '_wcpay_feature_allow_subscription_migrations' ); delete_option( '_wcpay_feature_custom_deposit_schedules' ); delete_option( '_wcpay_feature_account_overview_task_list' ); delete_option( '_wcpay_feature_account_overview' ); delete_option( '_wcpay_feature_sepa' ); delete_option( '_wcpay_feature_sofort' ); delete_option( '_wcpay_feature_giropay' ); delete_option( '_wcpay_feature_grouped_settings' ); delete_option( '_wcpay_feature_upe_settings_preview' ); delete_option( '_wcpay_feature_upe' ); delete_option( '_wcpay_feature_upe_split' ); delete_option( '_wcpay_feature_upe_deferred_intent' ); delete_option( '_wcpay_feature_dispute_on_transaction_page' ); delete_option( '_wcpay_feature_streamline_refunds' ); delete_option( 'wcpay_fraud_protection_settings_active' ); delete_option( '_wcpay_feature_mc_order_meta_helper' ); delete_option( '_wcpay_feature_pay_for_order_flow' ); delete_option( '_wcpay_feature_simplify_deposits_ui' ); delete_option( '_wcpay_fraud_protection_settings_enabled' ); delete_option( '_wcpay_feature_platform_checkout_subscriptions_enabled' ); delete_option( '_wcpay_feature_platform_checkout' ); delete_option( '_wcpay_feature_capital' ); delete_option( 'wcpay_capability_request_dismissed_notices' ); delete_option( 'wcpay_onboarding_eligibility_modal_dismissed' ); } } includes/migrations/class-migrate-payment-request-to-express-checkout-enabled.php 0000777 00000004151 15251156605 0024453 0 ustar 00 <?php /** * Class Migrate_Payment_Request_To_Express_Checkout_Enabled * * @package WooCommerce\Payments */ namespace WCPay\Migrations; use WC_Payments; defined( 'ABSPATH' ) || exit; /** * Class Migrate_Payment_Request_To_Express_Checkout_Enabled * * In PR #11144, Google Pay and Apple Pay were split into individual payment gateways. * In PR #11182, the `payment_request` setting on the card gateway was replaced with * individual `enabled` settings on each of the new gateways. * * This migration transfers the old `payment_request` value to the new gateway settings * and removes it from the card gateway. * * @see https://github.com/Automattic/woocommerce-payments/pull/11144 * @see https://github.com/Automattic/woocommerce-payments/pull/11182 * @since 10.4.0 */ class Migrate_Payment_Request_To_Express_Checkout_Enabled { /** * Version in which this migration was introduced. * * @var string */ const VERSION_SINCE = '10.4.0'; /** * Execute the migration if upgrading from a version before 10.4.0 * and the payment_request setting exists. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( self::VERSION_SINCE, $previous_version, '<=' ) ) { return; } $card_settings = get_option( 'woocommerce_woocommerce_payments_settings', [] ); if ( ! isset( $card_settings['payment_request'] ) ) { return; } $this->migrate( $card_settings ); } /** * Does the actual migration as described in the class docblock. * * @param array $card_settings The card gateway settings. */ private function migrate( $card_settings ) { $payment_request_enabled = ( $card_settings['payment_request'] ?? 'no' ) === 'yes' ? 'yes' : 'no'; update_option( 'woocommerce_woocommerce_payments_apple_pay_settings', [ 'enabled' => $payment_request_enabled ], true ); update_option( 'woocommerce_woocommerce_payments_google_pay_settings', [ 'enabled' => $payment_request_enabled ], true ); unset( $card_settings['payment_request'] ); update_option( 'woocommerce_woocommerce_payments_settings', $card_settings ); } } includes/migrations/class-delete-active-woopay-webhook.php 0000777 00000001425 15251156605 0020044 0 ustar 00 <?php /** * Class Delete_Active_WooPay_Webhook * * @package WooCommerce\Payments */ namespace WCPay\Migrations; defined( 'ABSPATH' ) || exit; /** * Class Delete_Active_WooPay_Webhook * * Fires an event on plugin upgrade to delete the active webhook. * Runs only once. We want to know whether existing install had it * enabled before the current version. */ class Delete_Active_WooPay_Webhook { /** * Checks whether we should trigger the event. */ public static function maybe_delete() { if ( version_compare( get_option( 'woocommerce_woocommerce_payments_version' ), '4.9.0', '>=' ) ) { return; } self::delete(); } /** * Deletes the active webhook. */ private static function delete() { \WCPay\WooPay\WooPay_Order_Status_Sync::remove_webhook(); } } includes/migrations/class-allowed-payment-request-button-types-update.php 0000777 00000004054 15251156605 0023105 0 ustar 00 <?php /** * Class Allowed_Payment_Request_Button_Types_Update * * @package WooCommerce\Payments */ namespace WCPay\Migrations; use WC_Payment_Gateway_WCPay; defined( 'ABSPATH' ) || exit; /** * Class Allowed_Payment_Request_Button_Types_Update * * Remaps deprecated payment request button types to fallback values. * * @since 2.6.0 */ class Allowed_Payment_Request_Button_Types_Update { /** * Version in which this migration was introduced. * * @var string */ const VERSION_SINCE = '2.6.0'; /** * WCPay gateway. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * Allowed_Payment_Request_Button_Types_Update constructor. * * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. */ public function __construct( WC_Payment_Gateway_WCPay $gateway ) { $this->gateway = $gateway; } /** * Only execute the migration if not applied yet. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( self::VERSION_SINCE, $previous_version, '>' ) ) { $this->migrate(); } } /** * Does the actual migration as described in the class docblock. */ private function migrate() { $button_type = $this->gateway->get_option( 'payment_request_button_type' ); $branded_type = $this->gateway->get_option( 'payment_request_button_branded_type' ); $this->gateway->update_option( 'payment_request_button_type', $this->map_button_type( $button_type, $branded_type ) ); } /** * Maps deprecated button types to fallback values. * * @param mixed $button_type "payment_request_button_type" value. * @param mixed $branded_type "payment_request_button_branded_type" value. * * @return mixed */ private function map_button_type( $button_type, $branded_type ) { if ( 'branded' === $button_type && 'short' === $branded_type ) { return 'default'; } if ( 'branded' === $button_type && 'short' !== $branded_type ) { return 'buy'; } if ( 'custom' === $button_type ) { return 'buy'; } return $button_type; } } includes/migrations/class-erase-bnpl-announcement-meta.php 0000777 00000001634 15251156605 0020027 0 ustar 00 <?php /** * Class Erase_Bnpl_Announcement_Meta * * @package WooCommerce\Payments */ namespace WCPay\Migrations; defined( 'ABSPATH' ) || exit; /** * Class Erase_Bnpl_Announcement_Meta * * Clearing up all meta data related to the BNPL April 2024 announcement. * * @since 8.1.0 */ class Erase_Bnpl_Announcement_Meta { /** * Checks whether it's worth doing the migration. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( '8.1.0', $previous_version, '>' ) ) { $this->migrate(); } } /** * Does the actual migration. */ private function migrate() { global $wpdb; delete_transient( 'wcpay_bnpl_april15_successful_purchases_count' ); // using a query directly, so we can delete the meta for all users. $wpdb->query( "DELETE FROM $wpdb->usermeta WHERE meta_key = '_wcpay_bnpl_april15_viewed'" ); } } includes/migrations/class-add-amazon-pay-to-express-checkout-locations.php 0000777 00000004406 15251156605 0023070 0 ustar 00 <?php /** * Class Add_Amazon_Pay_To_Express_Checkout_Locations * * @package WooCommerce\Payments */ namespace WCPay\Migrations; defined( 'ABSPATH' ) || exit; /** * Class Add_Amazon_Pay_To_Express_Checkout_Locations * * Adds Amazon Pay to express checkout location settings for existing installations. * * When Amazon Pay support is added, existing installations that already have * express_checkout_*_methods settings saved will not include 'amazon_pay' in * those arrays. This migration adds 'amazon_pay' to all locations so that * existing users get Amazon Pay enabled by default when they upgrade. * * @since 10.5.0 */ class Add_Amazon_Pay_To_Express_Checkout_Locations { /** * Version in which this migration was introduced. * * @var string */ const VERSION_SINCE = '10.5.0'; /** * The locations where express checkout buttons can be displayed. * * @var array */ const LOCATIONS = [ 'product', 'cart', 'checkout' ]; /** * Execute the migration if upgrading from a version before the migration version * and the express checkout settings exist without amazon_pay. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( self::VERSION_SINCE, $previous_version, '<=' ) ) { return; } $card_settings = get_option( 'woocommerce_woocommerce_payments_settings', [] ); // Only migrate if express checkout settings exist (meaning user has saved settings before). if ( ! isset( $card_settings['express_checkout_product_methods'] ) ) { return; } $this->migrate( $card_settings ); } /** * Adds amazon_pay to all express checkout location settings. * * @param array $card_settings The card gateway settings. */ private function migrate( $card_settings ) { $updated = false; foreach ( self::LOCATIONS as $location ) { $option_name = "express_checkout_{$location}_methods"; $methods = $card_settings[ $option_name ] ?? []; if ( ! in_array( 'amazon_pay', $methods, true ) ) { $methods[] = 'amazon_pay'; $card_settings[ $option_name ] = $methods; $updated = true; } } if ( $updated ) { update_option( 'woocommerce_woocommerce_payments_settings', $card_settings ); } } } includes/migrations/class-link-woopay-mutual-exclusion-handler.php 0000777 00000003474 15251156605 0021567 0 ustar 00 <?php /** * Class Link_WooPay_Mutual_Exclusion_Handler * * @package WooCommerce\Payments */ namespace WCPay\Migrations; use WC_Payment_Gateway_WCPay; use WC_Payments_Features; defined( 'ABSPATH' ) || exit; /** * Class Link_WooPay_Mutual_Exclusion_Handler * * In version 7.3.0, the logic responsible for disabling Stripe Link if WooPay is by default enabled, is moved from the gateways registration step to the migration. * * @since 7.3.0 */ class Link_WooPay_Mutual_Exclusion_Handler { /** * Version in which this migration was introduced. * * @var string */ const VERSION_SINCE = '7.3.0'; /** * WCPay gateway. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * Link_WooPay_Mutual_Exclusion_Handler constructor. * * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. */ public function __construct( WC_Payment_Gateway_WCPay $gateway ) { $this->gateway = $gateway; } /** * Only execute the migration if not applied yet. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( self::VERSION_SINCE, $previous_version, '>' ) ) { $this->migrate(); } } /** * Does the actual migration as described in the class docblock. */ private function migrate() { // check if both Stripe Link and WooPay are enabled and if so - disable Stripe Link. $enabled_payment_methods = $this->gateway->get_payment_method_ids_enabled_at_checkout(); $enabled_stripe_link_index = array_search( 'link', $enabled_payment_methods, true ); if ( false !== $enabled_stripe_link_index && WC_Payments_Features::is_woopay_enabled() ) { unset( $enabled_payment_methods[ $enabled_stripe_link_index ] ); $this->gateway->update_option( 'upe_enabled_payment_method_ids', $enabled_payment_methods ); } } } includes/migrations/class-update-service-data-from-server.php 0000777 00000003062 15251156605 0020454 0 ustar 00 <?php /** * Class Update_Service_Data_From_Server * * @package WooCommerce\Payments */ namespace WCPay\Migrations; defined( 'ABSPATH' ) || exit; /** * Class Update_Service_Data_From_Server * * Ensures that the service data is being updated after the upgrade to UPE. * Updating the service data ensures that the payment method fees are present. * If the fees are not present, the plugin shows "missing fees" text next to the UPE payment methods. * * @since 2.8.0 */ class Update_Service_Data_From_Server { /** * Instance to get information about the account. * * @var \WC_Payments_Account */ private $account; /** * Update_Service_Data_From_Server constructor. * * @param \WC_Payments_Account $account instance to get information about the account. */ public function __construct( \WC_Payments_Account $account ) { $this->account = $account; } /** * Checks whether it's worth doing the migration. */ public function maybe_migrate() { $account_data = $this->account->get_cached_account_data(); // no need to migrate anything, maybe the site is disconnected. // the plugin will eventually fetch new account data. if ( empty( $account_data ) ) { return; } // we have account data, do we have the fees for sofort/sepa/giropay/p24 etc? // if we do, no need to migrate. $account_fees = $this->account->get_fees(); if ( ! empty( $account_fees['giropay']['base'] ) ) { return; } $this->migrate(); } /** * Does the actual migration. */ private function migrate() { $this->account->refresh_account_data(); } } includes/migrations/class-migrate-express-checkout-locations.php 0000777 00000005774 15251156605 0021307 0 ustar 00 <?php /** * Class Migrate_Express_Checkout_Locations * * @package WooCommerce\Payments */ namespace WCPay\Migrations; defined( 'ABSPATH' ) || exit; /** * Class Migrate_Express_Checkout_Locations * * Migrates express checkout button location settings from method-centric to location-centric storage. * * Before: Each payment method stored its own list of enabled locations. * - `payment_request_button_locations` = ['product', 'cart', 'checkout'] (Apple Pay/Google Pay) * - `platform_checkout_button_locations` = ['product', 'cart'] (WooPay) * * After: Each location stores which payment methods are enabled there. * - `express_checkout_product_methods` = ['payment_request', 'woopay'] * - `express_checkout_cart_methods` = ['payment_request', 'woopay'] * - `express_checkout_checkout_methods` = ['payment_request'] * * @since 10.4.0 */ class Migrate_Express_Checkout_Locations { /** * Version in which this migration was introduced. * * @var string */ const VERSION_SINCE = '10.4.0'; /** * The locations where express checkout buttons can be displayed. * * @var array */ const LOCATIONS = [ 'product', 'cart', 'checkout' ]; /** * Execute the migration if upgrading from a version before the migration version * and the old settings exist. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( self::VERSION_SINCE, $previous_version, '<=' ) ) { return; } $card_settings = get_option( 'woocommerce_woocommerce_payments_settings', [] ); // Check if migration is needed - if old settings exist and new ones don't. $has_old_settings = isset( $card_settings['payment_request_button_locations'] ) || isset( $card_settings['platform_checkout_button_locations'] ); $has_new_settings = isset( $card_settings['express_checkout_product_methods'] ); if ( ! $has_old_settings || $has_new_settings ) { return; } $this->migrate( $card_settings ); } /** * Does the actual migration as described in the class docblock. * * @param array $card_settings The card gateway settings. */ private function migrate( $card_settings ) { // Get old settings with defaults. $payment_request_locations = $card_settings['payment_request_button_locations'] ?? self::LOCATIONS; $woopay_locations = $card_settings['platform_checkout_button_locations'] ?? self::LOCATIONS; // Build new location-centric settings. foreach ( self::LOCATIONS as $location ) { $methods = []; if ( in_array( $location, $payment_request_locations, true ) ) { $methods[] = 'payment_request'; } if ( in_array( $location, $woopay_locations, true ) ) { $methods[] = 'woopay'; } $card_settings[ "express_checkout_{$location}_methods" ] = $methods; } // Remove old settings. unset( $card_settings['payment_request_button_locations'] ); unset( $card_settings['platform_checkout_button_locations'] ); update_option( 'woocommerce_woocommerce_payments_settings', $card_settings ); } } includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php 0000777 00000062442 15251156605 0022206 0 ustar 00 <?php /** * Class WC_Payments_Remediate_Canceled_Auth_Fees * * @package WooCommerce\Payments\Migrations */ defined( 'ABSPATH' ) || exit; use WCPay\Constants\Intent_Status; /** * Remediates incorrect transaction fees and refunds for canceled authorizations. * * Between April 2023 / v5.8.0 (commit 2d13751) and November 2025 (commit 3058f59ad), * canceled authorizations incorrectly had transaction fees and refund objects created. * This migration cleans up those incorrect records. */ class WC_Payments_Remediate_Canceled_Auth_Fees { /** * Option key for tracking remediation status. */ const STATUS_OPTION_KEY = 'wcpay_fee_remediation_status'; /** * Option key for tracking last processed order ID. */ const LAST_ORDER_ID_OPTION_KEY = 'wcpay_fee_remediation_last_order_id'; /** * Option key for tracking current batch size. */ const BATCH_SIZE_OPTION_KEY = 'wcpay_fee_remediation_batch_size'; /** * Option key for tracking statistics. */ const STATS_OPTION_KEY = 'wcpay_fee_remediation_stats'; /** * Action Scheduler hook name. */ const ACTION_HOOK = 'wcpay_remediate_canceled_authorization_fees'; /** * Action Scheduler hook name for dry run. */ const DRY_RUN_ACTION_HOOK = 'wcpay_remediate_canceled_authorization_fees_dry_run'; /** * Action Scheduler hook for the async affected orders check. */ const CHECK_AFFECTED_ORDERS_HOOK = 'wcpay_check_affected_auth_fee_orders'; /** * Option key for tracking the affected orders check state. * * Possible values: * - false (option doesn't exist): not yet checked. * - 'scheduled': async check is scheduled or running. * - 'has_affected_orders': affected orders were found. * - 'no_affected_orders': no affected orders found. */ const CHECK_STATE_OPTION_KEY = 'wcpay_has_affected_auth_fee_orders'; /** * Option key for tracking dry run mode. */ const DRY_RUN_OPTION_KEY = 'wcpay_fee_remediation_dry_run'; /** * Starting batch size. */ const INITIAL_BATCH_SIZE = 20; /** * Minimum batch size. */ const MIN_BATCH_SIZE = 10; /** * Maximum batch size. */ const MAX_BATCH_SIZE = 100; /** * Target minimum execution time (seconds). */ const TARGET_MIN_TIME = 5; /** * Target maximum execution time (seconds). */ const TARGET_MAX_TIME = 20; /** * Bug introduction date (April 1, 2023 - commit 2d13751). */ const BUG_START_DATE = '2023-04-01'; /** * Constructor. */ public function __construct() { // Empty - call init() to register hooks. } /** * Initialize hooks. * * @return void */ public function init(): void { add_action( self::ACTION_HOOK, [ $this, 'process_batch' ] ); add_action( self::DRY_RUN_ACTION_HOOK, [ $this, 'process_batch_dry_run' ] ); add_action( self::CHECK_AFFECTED_ORDERS_HOOK, [ $this, 'check_and_cache_affected_orders' ] ); } /** * Check if dry run mode is enabled. * * @return bool True if dry run mode is enabled. */ public function is_dry_run(): bool { return (bool) get_option( self::DRY_RUN_OPTION_KEY, false ); } /** * Enable dry run mode. * * @return void */ private function enable_dry_run(): void { update_option( self::DRY_RUN_OPTION_KEY, true ); } /** * Disable dry run mode. * * @return void */ private function disable_dry_run(): void { delete_option( self::DRY_RUN_OPTION_KEY ); } /** * Check if remediation is complete. * * @return bool True if remediation is complete. */ public function is_complete(): bool { return 'completed' === get_option( self::STATUS_OPTION_KEY, '' ); } /** * Mark remediation as complete. * * @return void */ private function mark_complete(): void { update_option( self::STATUS_OPTION_KEY, 'completed' ); } /** * Mark remediation as running. * * @return void */ private function mark_running(): void { update_option( self::STATUS_OPTION_KEY, 'running' ); } /** * Get current batch size. * * @return int Current batch size. */ public function get_batch_size(): int { return (int) get_option( self::BATCH_SIZE_OPTION_KEY, self::INITIAL_BATCH_SIZE ); } /** * Update batch size. * * @param int $size New batch size. * @return void */ public function update_batch_size( int $size ): void { $size = max( self::MIN_BATCH_SIZE, min( self::MAX_BATCH_SIZE, $size ) ); update_option( self::BATCH_SIZE_OPTION_KEY, $size ); } /** * Get last processed order ID. * * @return int Last processed order ID. */ public function get_last_order_id(): int { return (int) get_option( self::LAST_ORDER_ID_OPTION_KEY, 0 ); } /** * Update last processed order ID. * * @param int $order_id Order ID. * @return void */ public function update_last_order_id( int $order_id ): void { update_option( self::LAST_ORDER_ID_OPTION_KEY, $order_id ); } /** * Get remediation statistics. * * @return array Statistics array with keys: processed, remediated, errors. */ public function get_stats(): array { $default = [ 'processed' => 0, 'remediated' => 0, 'errors' => 0, ]; $stats = get_option( self::STATS_OPTION_KEY, [] ); return array_merge( $default, $stats ); } /** * Increment a statistic counter. * * @param string $key Stat key to increment. * @return void */ public function increment_stat( string $key ): void { $stats = $this->get_stats(); if ( isset( $stats[ $key ] ) ) { ++$stats[ $key ]; update_option( self::STATS_OPTION_KEY, $stats ); } } /** * Clean up temporary remediation options. * * Preserves the status and stats options so merchants can see completion * information in the Tools page. Only removes temporary processing options. * * @return void */ private function cleanup(): void { // Delete only temporary processing options. // Keep STATUS_OPTION_KEY and STATS_OPTION_KEY so merchants can see completion info. delete_option( self::LAST_ORDER_ID_OPTION_KEY ); delete_option( self::BATCH_SIZE_OPTION_KEY ); delete_option( self::DRY_RUN_OPTION_KEY ); } /** * Clean up after dry run completes. * * Unlike the actual remediation cleanup, this removes ALL options including * status and stats, so merchants can still run the actual remediation afterward. * Dry run is just a preview - it shouldn't block the real action. * * @return void */ private function cleanup_dry_run(): void { delete_option( self::LAST_ORDER_ID_OPTION_KEY ); delete_option( self::BATCH_SIZE_OPTION_KEY ); delete_option( self::DRY_RUN_OPTION_KEY ); delete_option( self::STATUS_OPTION_KEY ); delete_option( self::STATS_OPTION_KEY ); } /** * Check if HPOS is enabled. * * This method is protected to allow mocking in tests. * * @return bool True if HPOS is enabled. */ protected function is_hpos_enabled(): bool { return WC_Payments_Utils::is_hpos_tables_usage_enabled(); } /** * Get affected orders that need remediation. * * @param int $limit Number of orders to retrieve. * @return WC_Order[] Array of WC_Order objects. */ public function get_affected_orders( int $limit ): array { if ( $this->is_hpos_enabled() ) { return $this->get_affected_orders_hpos( $limit ); } return $this->get_affected_orders_cpt( $limit ); } /** * Get affected orders using HPOS custom tables. * * @param int $limit Number of orders to retrieve. * @return WC_Order[] Array of WC_Order objects. */ private function get_affected_orders_hpos( int $limit ): array { global $wpdb; $last_order_id = $this->get_last_order_id(); $orders_table = $wpdb->prefix . 'wc_orders'; $meta_table = $wpdb->prefix . 'wc_orders_meta'; $sql = "SELECT orders.id FROM {$orders_table} orders INNER JOIN {$meta_table} status_meta ON orders.id = status_meta.order_id AND status_meta.meta_key = '_intention_status' AND status_meta.meta_value = %s LEFT JOIN {$meta_table} fees_meta ON orders.id = fees_meta.order_id AND fees_meta.meta_key = '_wcpay_transaction_fee' WHERE orders.type = 'shop_order' AND orders.date_created_gmt >= %s AND ( -- Refunded with or without a refund. orders.status = 'wc-refunded' -- Cancelled with fees. OR ( orders.status = 'wc-cancelled' AND fees_meta.order_id IS NOT NULL ) )"; $params = [ Intent_Status::CANCELED, self::BUG_START_DATE ]; // Add offset based on last order ID. if ( $last_order_id > 0 ) { $sql .= ' AND orders.id > %d'; $params[] = $last_order_id; } // Add ordering and limit. $sql .= ' ORDER BY orders.id ASC LIMIT %d'; $params[] = $limit; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $order_ids = $wpdb->get_col( $wpdb->prepare( $sql, $params ) ); return $this->convert_ids_to_orders( $order_ids ); } /** * Get affected orders using CPT (posts) storage. * * @param int $limit Number of orders to retrieve. * @return WC_Order[] Array of WC_Order objects. */ private function get_affected_orders_cpt( int $limit ): array { global $wpdb; $last_order_id = $this->get_last_order_id(); // Build the SQL query to find orders with canceled intent status that have either: // 1. Incorrect fee metadata (_wcpay_transaction_fee or _wcpay_net), OR // 2. Refund objects (which shouldn't exist for never-captured authorizations), OR // 3. Incorrect order status of 'wc-refunded' (should be 'wc-cancelled'). $sql = "SELECT orders.ID FROM {$wpdb->posts} orders INNER JOIN {$wpdb->postmeta} status_meta ON orders.ID = status_meta.post_id AND status_meta.meta_key = '_intention_status' AND status_meta.meta_value = %s LEFT JOIN {$wpdb->postmeta} fees_meta ON orders.ID = fees_meta.post_id AND fees_meta.meta_key = '_wcpay_transaction_fee' WHERE orders.post_type IN ('shop_order', 'shop_order_placeholder') AND orders.post_date >= %s AND ( -- Refunded with or without a refund. orders.post_status = 'wc-refunded' -- Cancelled with fees OR ( orders.post_status = 'wc-cancelled' AND fees_meta.post_id IS NOT NULL ) )"; $params = [ Intent_Status::CANCELED, self::BUG_START_DATE ]; // Add offset based on last order ID. if ( $last_order_id > 0 ) { $sql .= ' AND orders.ID > %d'; $params[] = $last_order_id; } // Add ordering and limit. $sql .= ' ORDER BY orders.ID ASC LIMIT %d'; $params[] = $limit; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $order_ids = $wpdb->get_col( $wpdb->prepare( $sql, $params ) ); return $this->convert_ids_to_orders( $order_ids ); } /** * Convert order IDs to WC_Order objects. * * @param array $order_ids Array of order IDs. * @return WC_Order[] Array of WC_Order objects. */ private function convert_ids_to_orders( array $order_ids ): array { $orders = []; foreach ( $order_ids as $order_id ) { $order = wc_get_order( $order_id ); if ( $order instanceof WC_Order ) { $orders[] = $order; } } return $orders; } /** * Adjust batch size based on execution time. * * @param float $execution_time Execution time in seconds. * @return void */ public function adjust_batch_size( float $execution_time ): void { $current_size = $this->get_batch_size(); if ( $execution_time < self::TARGET_MIN_TIME ) { // Too fast - double batch size. $this->update_batch_size( $current_size * 2 ); } elseif ( $execution_time > self::TARGET_MAX_TIME ) { // Too slow - halve batch size. $this->update_batch_size( (int) ( $current_size / 2 ) ); } // Otherwise, keep current size. } /** * Process a batch of orders. * * @return void */ public function process_batch(): void { // Check if already complete. if ( $this->is_complete() ) { return; } // This can affect the order transitions by unnecessarily reaching out to Stripe. remove_action( 'woocommerce_order_status_cancelled', [ WC_Payments::get_order_service(), 'cancel_authorizations_on_order_status_change' ] ); $start_time = microtime( true ); $batch_size = $this->get_batch_size(); $orders = $this->get_affected_orders( $batch_size ); // If no orders found, mark as complete. if ( empty( $orders ) ) { $this->mark_complete(); $this->log_completion(); $this->cleanup(); return; } // Process each order. foreach ( $orders as $order ) { $this->increment_stat( 'processed' ); if ( $this->remediate_order( $order ) ) { $this->increment_stat( 'remediated' ); wc_get_logger()->info( sprintf( 'Remediated order %d', $order->get_id() ), [ 'source' => 'wcpay-fee-remediation' ] ); } else { $this->increment_stat( 'errors' ); } // Update last order ID. $this->update_last_order_id( $order->get_id() ); } // Adjust batch size based on execution time. $execution_time = microtime( true ) - $start_time; $this->adjust_batch_size( $execution_time ); // Log batch completion. wc_get_logger()->info( sprintf( 'Processed batch of %d orders in %.2f seconds. New batch size: %d', count( $orders ), $execution_time, $this->get_batch_size() ), [ 'source' => 'wcpay-fee-remediation' ] ); // Always schedule next batch to check for more orders. // The batch will complete when get_affected_orders() returns empty. // This is more reliable than checking count vs batch_size, which can // incorrectly mark complete if the query returns fewer orders due to // transient issues (DB performance, caching, etc.). $this->schedule_next_batch(); } /** * Process a batch of orders in dry run mode. * * @return void */ public function process_batch_dry_run(): void { // Check if already complete (but only if not in dry run mode - dry run uses separate tracking). if ( $this->is_complete() && ! $this->is_dry_run() ) { return; } $batch_size = $this->get_batch_size(); $orders = $this->get_affected_orders( $batch_size ); // If no orders found, dry run is done. if ( empty( $orders ) ) { $this->log_completion_dry_run(); $this->cleanup_dry_run(); return; } // Process each order in dry run mode. foreach ( $orders as $order ) { $this->increment_stat( 'processed' ); if ( $this->remediate_order( $order, true ) ) { $this->increment_stat( 'remediated' ); } else { $this->increment_stat( 'errors' ); } // Update last order ID. $this->update_last_order_id( $order->get_id() ); } // Log batch completion. wc_get_logger()->info( sprintf( '[DRY RUN] Processed batch of %d orders.', count( $orders ) ), [ 'source' => 'wcpay-fee-remediation' ] ); // Always schedule next batch to check for more orders. // The batch will complete when get_affected_orders() returns empty. $this->schedule_next_batch_dry_run(); } /** * Schedule the next batch. * * @return void */ private function schedule_next_batch(): void { if ( ! function_exists( 'as_schedule_single_action' ) ) { wc_get_logger()->warning( 'Action Scheduler is not available. Cannot schedule next batch for fee remediation.', [ 'source' => 'wcpay-fee-remediation' ] ); return; } as_schedule_single_action( time() + 60, // 1 minute from now. self::ACTION_HOOK, [], 'woocommerce-payments' ); } /** * Schedule the next batch for dry run. * * @return void */ private function schedule_next_batch_dry_run(): void { if ( ! function_exists( 'as_schedule_single_action' ) ) { wc_get_logger()->warning( 'Action Scheduler is not available. Cannot schedule next batch for fee remediation dry run.', [ 'source' => 'wcpay-fee-remediation' ] ); return; } as_schedule_single_action( time() + 60, // 1 minute from now. self::DRY_RUN_ACTION_HOOK, [], 'woocommerce-payments' ); } /** * Log completion. * * @return void */ private function log_completion(): void { $stats = $this->get_stats(); wc_get_logger()->info( sprintf( 'Remediation complete. Processed: %d, Remediated: %d, Errors: %d', $stats['processed'], $stats['remediated'], $stats['errors'] ), [ 'source' => 'wcpay-fee-remediation' ] ); } /** * Log completion for dry run. * * @return void */ private function log_completion_dry_run(): void { $stats = $this->get_stats(); wc_get_logger()->info( sprintf( '[DRY RUN] Complete. Found %d orders that would be remediated. No changes were made. Check the WooCommerce logs for details on each order.', $stats['remediated'] ), [ 'source' => 'wcpay-fee-remediation' ] ); } /** * Remediate a single order. * * @param WC_Order $order Order to remediate. * @param bool $dry_run If true, only log what would be changed without modifying data. * @return bool True on success, false on failure. */ public function remediate_order( WC_Order $order, bool $dry_run = false ): bool { try { // Capture current values for the note. $fee = $order->get_meta( '_wcpay_transaction_fee', true ); $net = $order->get_meta( '_wcpay_net', true ); $refunds = $order->get_refunds(); $current_status = $order->get_status(); // Only delete refunds that were created by WCPay (have _wcpay_refund_id metadata). // This avoids deleting manually-created refunds or refunds from other plugins. $wcpay_refunds = $this->get_wcpay_refunds( $refunds ); $wcpay_refund_count = count( $wcpay_refunds ); $wcpay_refund_total = 0; // Calculate refund IDs and totals. $refund_ids = []; foreach ( $wcpay_refunds as $refund ) { $wcpay_refund_total += abs( $refund->get_amount() ); $refund_ids[] = $refund->get_id(); } // Check if status would change. $would_change_status = 'refunded' === $current_status; // Build log message for dry run or note for actual remediation. $changes = []; if ( $would_change_status ) { $changes[] = 'Changed order status from "Refunded" to "Cancelled"'; } if ( $wcpay_refund_count > 0 ) { $changes[] = sprintf( 'Deleted %d WooPayments refund object%s (IDs: %s) totaling %s', $wcpay_refund_count, $wcpay_refund_count > 1 ? 's' : '', implode( ', ', $refund_ids ), wc_price( $wcpay_refund_total, [ 'currency' => $order->get_currency() ] ) ); } if ( ! empty( $fee ) ) { $changes[] = sprintf( 'Removed transaction fee: %s', wc_price( $fee, [ 'currency' => $order->get_currency() ] ) ); } if ( ! empty( $net ) ) { $changes[] = sprintf( 'Removed net amount: %s', wc_price( $net, [ 'currency' => $order->get_currency() ] ) ); } // In dry run mode, just log what would happen and return. if ( $dry_run ) { if ( ! empty( $changes ) ) { wc_get_logger()->info( sprintf( '[DRY RUN] Order %d would be remediated: %s', $order->get_id(), wp_strip_all_tags( implode( '; ', $changes ) ) ), [ 'source' => 'wcpay-fee-remediation' ] ); } return true; } // Actually perform the remediation. $parent_order_id = $order->get_id(); foreach ( $wcpay_refunds as $refund ) { $refund_id = $refund->get_id(); // Delete refund stats BEFORE deleting the refund (while it still exists). // We do this proactively because the woocommerce_before_delete_order hook // may not have its handlers registered in Action Scheduler context. $this->delete_order_stats( $refund_id ); $refund->delete( true ); // Force delete, bypass trash. // Fire the hook WC expects for refund deletion. do_action( 'woocommerce_refund_deleted', $refund_id, $parent_order_id ); } // Remove fee metadata from the order. $order->delete_meta_data( '_wcpay_transaction_fee' ); $order->delete_meta_data( '_wcpay_net' ); $order->delete_meta_data( '_wcpay_refund_id' ); $order->delete_meta_data( '_wcpay_refund_status' ); // Fix incorrect order status: 'refunded' should be 'cancelled' for never-captured authorizations. if ( $would_change_status ) { $order->set_status( 'cancelled', '', false ); // Don't trigger status change emails. } // Build detailed note. $note_parts = [ 'Removed incorrect data from canceled authorization:' ]; foreach ( $changes as $change ) { $note_parts[] = '- ' . $change; } $note_parts[] = ''; $note_parts[] = 'These records were incorrectly created for an authorization that was never captured.'; $note_parts[] = 'No actual payment or refund occurred.'; $order->add_order_note( implode( "\n", $note_parts ) ); $order->save(); // Fallback sync in case the woocommerce_refund_deleted hook doesn't // fully update analytics for edge cases. $this->sync_order_stats( $order->get_id() ); return true; } catch ( Exception $e ) { // Log error but don't throw - let calling code handle retry. wc_get_logger()->error( sprintf( 'Failed to remediate order %d: %s', $order->get_id(), $e->getMessage() ), [ 'source' => 'wcpay-fee-remediation' ] ); return false; } } /** * Filter refunds to only include those created by WooPayments. * * WooPayments-created refunds have the _wcpay_refund_id metadata. * This ensures we don't delete manually-created refunds or refunds from other plugins. * * @param WC_Order_Refund[] $refunds Array of refund objects. * @return WC_Order_Refund[] Array of WooPayments-created refunds. */ private function get_wcpay_refunds( array $refunds ): array { return array_filter( $refunds, function ( $refund ) { // Check if this refund was created by WCPay (has the refund ID metadata). $wcpay_refund_id = $refund->get_meta( '_wcpay_refund_id', true ); return ! empty( $wcpay_refund_id ); } ); } /** * Sync order stats to WooCommerce Analytics. * * Fallback sync in case the woocommerce_refund_deleted hook doesn't * fully update analytics for edge cases. * * @param int $order_id Order ID to sync. * @return void */ protected function sync_order_stats( int $order_id ): void { // Check if the OrdersStatsDataStore class exists (requires WooCommerce Admin / WooCommerce 4.0+). if ( ! class_exists( 'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore' ) ) { return; } try { \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore::sync_order( $order_id ); } catch ( Exception $e ) { // Log but don't fail - analytics sync is not critical. wc_get_logger()->warning( sprintf( 'Failed to sync order %d to analytics: %s', $order_id, $e->getMessage() ), [ 'source' => 'wcpay-fee-remediation' ] ); } } /** * Delete order stats from WooCommerce Analytics. * * Uses WooCommerce's DataStore::delete_order() API to properly remove * the order/refund stats row from the wc_order_stats table. * * This must be called BEFORE the refund is deleted, while it still exists, * so the WC API can perform its internal checks. * * @param int $order_id Order or refund ID to delete stats for. * @return void */ protected function delete_order_stats( int $order_id ): void { // Check if the DataStore class exists (requires WooCommerce Admin / WooCommerce 4.0+). if ( ! class_exists( 'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore' ) ) { return; } try { // Use WooCommerce's proper API to delete the stats row. // This handles all internal state management and fires appropriate hooks. \Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore::delete_order( $order_id ); wc_get_logger()->info( sprintf( 'Deleted stats row for refund %d via WC DataStore API', $order_id ), [ 'source' => 'wcpay-fee-remediation' ] ); } catch ( Exception $e ) { // Log but don't fail - analytics cleanup is not critical. wc_get_logger()->warning( sprintf( 'Failed to delete stats for order %d: %s', $order_id, $e->getMessage() ), [ 'source' => 'wcpay-fee-remediation' ] ); } } /** * Schedule remediation to run in the background. * * This is the public method called from the WooCommerce Tools page. * * @return void */ public function schedule_remediation(): void { // Mark as running and schedule first batch. $this->mark_running(); $this->disable_dry_run(); if ( function_exists( 'as_schedule_single_action' ) ) { as_schedule_single_action( time() + 10, // Start in 10 seconds. self::ACTION_HOOK, [], 'woocommerce-payments' ); wc_get_logger()->info( 'Scheduled fee remediation from WooCommerce Tools.', [ 'source' => 'wcpay-fee-remediation' ] ); } } /** * Schedule dry run to preview what would be remediated. * * This allows merchants to see what orders would be affected before * committing to the actual remediation. * * @return void */ public function schedule_dry_run(): void { // Mark as running and enable dry run mode. $this->mark_running(); $this->enable_dry_run(); if ( function_exists( 'as_schedule_single_action' ) ) { as_schedule_single_action( time() + 10, // Start in 10 seconds. self::DRY_RUN_ACTION_HOOK, [], 'woocommerce-payments' ); wc_get_logger()->info( 'Scheduled fee remediation DRY RUN from WooCommerce Tools.', [ 'source' => 'wcpay-fee-remediation' ] ); } } /** * Check if there are any orders that need remediation. * * @return bool True if there are affected orders. */ public function has_affected_orders(): bool { $orders = $this->get_affected_orders( 1 ); return ! empty( $orders ); } /** * Run the affected orders query and cache the result. * * Called by Action Scheduler in a separate request. * * @return void */ public function check_and_cache_affected_orders(): void { $result = $this->has_affected_orders(); update_option( self::CHECK_STATE_OPTION_KEY, $result ? 'has_affected_orders' : 'no_affected_orders', true ); } } includes/migrations/class-additional-payment-methods-admin-notes-removal.php 0000777 00000002443 15251156605 0023465 0 ustar 00 <?php /** * Class Allowed_Payment_Request_Button_Sizes_Update * * @package WooCommerce\Payments */ namespace WCPay\Migrations; use WC_Payments_Notes_Additional_Payment_Methods; defined( 'ABSPATH' ) || exit; /** * Class Allowed_Payment_Request_Button_Sizes_Update * * In version 7.0.0, the UPE is further cleaned up and it is also the default now. This migration removes the admin notes for the additional payment methods. * * @since 7.0.0 */ class Additional_Payment_Methods_Admin_Notes_Removal { /** * Version in which this migration was introduced. * * @var string */ const VERSION_SINCE = '7.0.0'; /** * Only execute the migration if not applied yet. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( self::VERSION_SINCE, $previous_version, '>' ) ) { $this->migrate(); } } /** * Does the actual migration as described in the class docblock. */ private function migrate() { // Delete UPE admin notes as UPE is the default now. This is part of the migration and should be removed in the future. require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-additional-payment-methods.php'; WC_Payments_Notes_Additional_Payment_Methods::possibly_delete_note(); } } includes/migrations/class-gateway-settings-sync.php 0000777 00000004011 15251156605 0016624 0 ustar 00 <?php /** * Class Gateway_Settings_Sync * * @package WooCommerce\Payments */ namespace WCPay\Migrations; use WC_Payment_Gateway_WCPay; defined( 'ABSPATH' ) || exit; /** * Class Delete_Active_WooPay_Webhook * * Aligns settings object for every gateway to support new approach of settings handling without the need of using the settings controller. */ class Gateway_Settings_Sync { /** * Version in which this migration was introduced. * * @var string */ const VERSION_SINCE = '7.4.0'; /** * WCPay gateway. * * @var WC_Payment_Gateway_WCPay */ private $main_gateway; /** * All registered gateways. * * @var array */ private $all_registered_gateways; /** * Gateway_Settings_Sync constructor. * * @param WC_Payment_Gateway_WCPay $main_gateway WCPay gateway. * @param array $all_registered_gateways All registered gateways. */ public function __construct( WC_Payment_Gateway_WCPay $main_gateway, $all_registered_gateways ) { $this->main_gateway = $main_gateway; $this->all_registered_gateways = $all_registered_gateways; } /** * Checks whether we should trigger the event. */ public function maybe_sync() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( self::VERSION_SINCE, $previous_version, '>' ) ) { $this->sync(); } } /** * Syncs gateway setting objects. */ private function sync() { $enabled_payment_methods = $this->main_gateway->get_option( 'upe_enabled_payment_method_ids', [] ); foreach ( $this->all_registered_gateways as $gateway ) { // Skip the main gateway as it's settings are already in sync. if ( 'card' !== $gateway->get_stripe_id() ) { if ( in_array( $gateway->get_stripe_id(), $enabled_payment_methods, true ) ) { $gateway->enable(); $gateway->update_option( 'upe_enabled_payment_method_ids', $enabled_payment_methods ); } else { $gateway->update_option( 'upe_enabled_payment_method_ids', $enabled_payment_methods ); } } } } } includes/migrations/class-manual-capture-payment-method-settings-update.php 0000777 00000004537 15251156605 0023355 0 ustar 00 <?php /** * Class Manual_Capture_Payment_Method_Settings_Update * * @package WooCommerce\Payments */ namespace WCPay\Migrations; use WCPay\Constants\Payment_Method; use WC_Payment_Gateway_WCPay; defined( 'ABSPATH' ) || exit; /** * Class Manual_Capture_Payment_Method_Settings_Update * * Updates payment methods when manual capture is enabled. */ class Manual_Capture_Payment_Method_Settings_Update { /** * Version in which this migration was introduced. * * @var string */ const VERSION_SINCE = '8.3.0'; /** * WCPay gateway. * * @var WC_Payment_Gateway_WCPay */ private $main_gateway; /** * All registered gateways. * * @var array */ private $all_registered_gateways; /** * Manual_Capture_Payment_Method_Settings_Update constructor. * * @param WC_Payment_Gateway_WCPay $main_gateway WCPay gateway. * @param array $all_registered_gateways All registered gateways. */ public function __construct( WC_Payment_Gateway_WCPay $main_gateway, $all_registered_gateways ) { $this->main_gateway = $main_gateway; $this->all_registered_gateways = $all_registered_gateways; } /** * Checks whether we should trigger the event. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( self::VERSION_SINCE, $previous_version, '>' ) ) { $this->migrate(); } } /** * Disables payment methods that do not support manual capture, when manual capture is enabled and updates * the enabled payment methods for each gateway. */ private function migrate() { $enabled_payment_methods = $this->main_gateway->get_option( 'upe_enabled_payment_method_ids', [] ); $is_manual_capture_enabled = 'yes' === $this->main_gateway->get_option( 'manual_capture' ); if ( $is_manual_capture_enabled ) { $filtered_payment_methods = array_filter( $enabled_payment_methods, function ( $method ) { return in_array( $method, [ Payment_Method::CARD, Payment_Method::LINK ], true ); } ); foreach ( $this->all_registered_gateways as $gateway ) { $stripe_id = $gateway->get_stripe_id(); if ( Payment_Method::CARD !== $stripe_id && Payment_Method::LINK !== $stripe_id ) { $gateway->disable(); } $gateway->update_option( 'upe_enabled_payment_method_ids', $filtered_payment_methods ); } } } } includes/migrations/class-payment-method-deprecation-settings-update.php 0000777 00000005352 15251156605 0022730 0 ustar 00 <?php /** * Class Payment_Method_Deprecation_Settings_Update * * @package WooCommerce\Payments */ namespace WCPay\Migrations; use WC_Payment_Gateway_WCPay; defined( 'ABSPATH' ) || exit; /** * Class Payment_Method_Deprecation_Settings_Update * * Aligns settings object for every gateway to support new approach of settings handling without the need of using the settings controller. */ class Payment_Method_Deprecation_Settings_Update { /** * WCPay gateway. * * @var WC_Payment_Gateway_WCPay */ private $main_gateway; /** * All registered gateways. * * @var array */ private $all_registered_gateways; /** * Payment method type ID. * * @var string */ private $payment_method_id; /** * Version in which this migration was introduced. * * @var string */ private $migration_version; /** * Gateway_Settings_Sync constructor. * * @param WC_Payment_Gateway_WCPay $main_gateway WCPay gateway. * @param array $all_registered_gateways All registered gateways. * @param string $payment_method_id Stripe payment method ID of payment method to deprecate. * @param string $migration_version Plugin version after which migration should run. */ public function __construct( WC_Payment_Gateway_WCPay $main_gateway, $all_registered_gateways, $payment_method_id, $migration_version ) { $this->main_gateway = $main_gateway; $this->all_registered_gateways = $all_registered_gateways; $this->payment_method_id = $payment_method_id; $this->migration_version = $migration_version; } /** * Checks whether we should trigger the event. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( $this->migration_version, $previous_version, '>' ) ) { $this->migrate(); } } /** * Syncs gateway setting objects. */ private function migrate() { $enabled_payment_methods = $this->main_gateway->get_option( 'upe_enabled_payment_method_ids', [] ); $filtered_payment_methods = array_filter( $enabled_payment_methods, function ( $method ) { return $this->payment_method_id !== $method; } ); foreach ( $this->all_registered_gateways as $gateway ) { if ( $this->payment_method_id === $gateway->get_stripe_id() ) { if ( in_array( $gateway->get_stripe_id(), $enabled_payment_methods, true ) ) { $gateway->disable(); $gateway->update_option( 'upe_enabled_payment_method_ids', $filtered_payment_methods ); } else { $gateway->update_option( 'upe_enabled_payment_method_ids', $filtered_payment_methods ); } } else { $gateway->update_option( 'upe_enabled_payment_method_ids', $filtered_payment_methods ); } } } } includes/migrations/class-allowed-payment-request-button-sizes-update.php 0000777 00000002727 15251156605 0023103 0 ustar 00 <?php /** * Class Allowed_Payment_Request_Button_Sizes_Update * * @package WooCommerce\Payments */ namespace WCPay\Migrations; use WC_Payment_Gateway_WCPay; defined( 'ABSPATH' ) || exit; /** * Class Allowed_Payment_Request_Button_Sizes_Update * * In version 6.9.0, the "default" size got renamed to "small" - ensure that the settings are up-to-date for existing merchants. * * @since 6.9.0 */ class Allowed_Payment_Request_Button_Sizes_Update { /** * Version in which this migration was introduced. * * @var string */ const VERSION_SINCE = '6.9.0'; /** * WCPay gateway. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * Allowed_Payment_Request_Button_Sizes_Update constructor. * * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. */ public function __construct( WC_Payment_Gateway_WCPay $gateway ) { $this->gateway = $gateway; } /** * Only execute the migration if not applied yet. */ public function maybe_migrate() { $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); if ( version_compare( self::VERSION_SINCE, $previous_version, '>' ) ) { $this->migrate(); } } /** * Does the actual migration as described in the class docblock. */ private function migrate() { $button_size = $this->gateway->get_option( 'payment_request_button_size' ); if ( 'default' === $button_size ) { $this->gateway->update_option( 'payment_request_button_size', 'small' ); } } } includes/in-person-payments/class-wc-payments-printed-receipt-sample-order.php 0000777 00000002704 15251156605 0023711 0 ustar 00 <?php /** * Class WC_Payments_Printed_Receipt_Sample_Order * * @package WooCommerce\Payments */ use WCPay\Constants\Currency_Code; defined( 'ABSPATH' ) || exit; /** * This class represents a sample order to be used when generating a preview of a printed receipt. * * @see WC_REST_Payments_Reader_Controller::preview_print_receipt */ class WC_Payments_Printed_Receipt_Sample_Order extends WC_Order { const PREVIEW_RECEIPT_ORDER_DATA = [ 'id' => '42', 'currency' => Currency_Code::UNITED_STATES_DOLLAR, 'subtotal' => 0, 'line_items' => [ [ 'name' => 'Sample', 'quantity' => 1, 'subtotal' => 0, 'product' => [ 'price' => 0, 'regular_price' => 1, 'id' => 'sample', ], ], [ 'name' => 'Sample', 'quantity' => 1, 'subtotal' => 0, 'product' => [ 'price' => 0, 'regular_price' => 1, 'id' => 'sample', ], ], ], 'coupon_lines' => [ [ 'code' => 'DISCOUNT', 'description' => 'sample', 'discount' => 0, ], ], 'tax_lines' => [ [ 'rate_percent' => 0, 'tax_total' => '0', ], ], 'total' => 0, 'shipping_tax' => 0, 'total_fees' => 0, ]; /** * __construct */ public function __construct() { // noop. } /** * Returns order data * * @return array */ public function get_data(): array { return self::PREVIEW_RECEIPT_ORDER_DATA; } } includes/in-person-payments/templates/html-in-person-payment-receipt.php 0000777 00000016665 15251156605 0022644 0 ustar 00 <?php /** * In Person Payments Receipt Template * * @package WooCommerce\Payments */ /** * Helper to generate markup to render a price. * * @param array $product The product to display. * @param string $currency The currency to display. * @return string */ function wcpay_format_price_helper( array $product, string $currency ): string { $active_price = $product['price']; $regular_price = $product['regular_price']; $has_discount = $active_price !== $regular_price; if ( $has_discount ) { return '<s>' . wc_price( $regular_price, [ 'currency' => $currency ] ) . '</s> ' . wc_price( $active_price, [ 'currency' => $currency ] ); } return wc_price( $active_price, [ 'currency' => $currency ] ); } ?><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Print Receipt</title> <style> body { margin: 0; padding: 0; border: 0; } .align-left { text-align: left; } .align-right { text-align: right; } .align-top { vertical-align: top; } .receipt { min-width: 130px; max-width: 300px; margin: 0 auto; text-align: center; font-family: SF Pro Text, sans-serif; font-size: 10px; } .receipt-table { width: 100%; border-collapse: separate; border-spacing: 0 2px; font-size: 10px; } .receipt__header .title { font-size: 14px; line-height: 17px; margin-bottom: 12px; margin-top: 12px; font-weight: 700; } .receipt__header .store { padding: 0 12px; } .receipt__header .store__address { margin-top: 12px; line-height: 2px; } .receipt__header .store__contact { margin-top: 4px; } .receipt__header .order__title { font-weight: 800; } .receipt__transaction { line-height: 2px; } .branding-logo { max-width: 250px; margin: 20px auto; } #powered_by { font-size: 7px; padding-top: 5px; } </style> </head> <body> <div class="receipt"> <div class="receipt__header"> <?php if ( ! empty( $branding_logo['content_type'] ) ) { ?> <img class="branding-logo" src="data:<?php echo esc_html( $branding_logo['content_type'] ); ?>;base64,<?php echo esc_html( $branding_logo['file_content'] ); ?>" alt="<?php echo esc_html( $business_name ); ?>"/> <?php } ?> <h1 class="title"><?php echo esc_html( $business_name ); ?></h1> <hr /> <div class="store"> <?php if ( $support_address ) { ?> <div class="store__address"> <p><?php echo esc_html( $support_address['line1'] ); ?></p> <p><?php echo esc_html( $support_address['line2'] ); ?></p> <p><?php echo esc_html( implode( ' ', [ $support_address['city'], $support_address['state'], $support_address['postal_code'], $support_address['country'] ] ) ); ?></p> <?php echo esc_html( gmdate( 'Y/m/d - H:iA' ) ); ?> </div> <?php } ?> <p class="store__contact"> <?php echo esc_html( implode( ' ', [ $support_phone, $support_email ] ) ); ?> </p> </div> <div class="order"> <p class="order__title"><?php printf( '%s %s', esc_html__( 'Order', 'woocommerce-payments' ), esc_html( $order['id'] ) ); ?></p> </div> </div> <hr /> <div class="receipt__products"> <table class="receipt-table"> <?php foreach ( $line_items as $item ) { ?> <tr> <td class="align-left"> <div><?php echo esc_html( $item['name'] ); ?></div> <div><?php echo esc_html( $item['quantity'] ); ?> @ <?php echo wp_kses( wcpay_format_price_helper( $item['product'], $order['currency'] ), 'post' ); ?></div> <div><?php printf( '%s: %s', esc_html__( 'SKU', 'woocommerce-payments' ), esc_html( $item['product']['id'] ) ); ?></div> <!-- TODO SKU or ID? --> </td> <td class="align-right align-top"><?php echo wp_kses( wc_price( $item['subtotal'], [ 'currency' => $order['currency'] ] ), 'post' ); ?></td> </tr> <?php } ?> </table> </div> <hr /> <div class="receipt__subtotal"> <table class="receipt-table"> <tr> <td class="align-left"><b><?php echo esc_html__( 'SUBTOTAL', 'woocommerce-payments' ); ?></b></td> <td class="align-right"><b><?php echo wp_kses( wc_price( $order['subtotal'], [ 'currency' => $order['currency'] ] ), 'post' ); ?></b></td> </tr> <?php foreach ( $coupon_lines as $order_coupon ) { ?> <tr> <td class="align-left"> <div><?php printf( '%s: %s', esc_html__( 'Discount', 'woocommerce-payments' ), esc_html( $order_coupon['code'] ) ); ?></div> <div><?php echo esc_html( $order_coupon['description'] ); ?></div> </td> <td class="align-right align-top"><?php echo wp_kses( wc_price( abs( $order_coupon['discount'] ) * -1, [ 'currency' => $order['currency'] ] ), 'post' ); ?></td> </tr> <?php } ?> <?php if ( 0 < $order['total_fees'] ) : ?> <tr> <td class="align-left"><?php esc_html_e( 'Fees:', 'woocommerce-payments' ); ?></td> <td class="align-right align-top"> <?php echo wp_kses( wc_price( $order['total_fees'], [ 'currency' => $order['currency'] ] ), 'post' ); ?> </td> </tr> <?php endif; ?> <?php if ( 0 < $order['shipping_tax'] ) : ?> <tr> <td class="align-left"><?php esc_html_e( 'Shipping:', 'woocommerce-payments' ); ?></td> <td class="align-right align-top"> <?php echo wp_kses( wc_price( $order['shipping_tax'], [ 'currency' => $order['currency'] ] ), 'post' ); ?> </td> </tr> <?php endif; ?> <?php foreach ( $tax_lines as $tax_line ) { ?> <tr> <td class="align-left"> <div><?php echo esc_html__( 'Tax', 'woocommerce-payments' ); ?></div> <div><?php echo esc_html( wc_round_tax_total( $tax_line['rate_percent'] ) ); ?>%</div> </td> <td class="align-right align-top"><?php echo wp_kses( wc_price( $tax_line['tax_total'] + $tax_line['shipping_tax_total'], [ 'currency' => $order['currency'] ] ), 'post' ); ?></td> </tr> <?php } ?> <tr> <td colspan="2" class="align-left"></td> </tr> <tr> <td class="align-left"><b><?php echo esc_html__( 'TOTAL', 'woocommerce-payments' ); ?></b></td> <td class="align-right"><b><?php echo wp_kses( wc_price( $order['total'], [ 'currency' => $order['currency'] ] ), 'post' ); ?></b></td> </tr> </table> </div> <hr /> <div class="receipt__amount-paid"> <table class="receipt-table"> <tr> <td class="align-left"><b><?php echo esc_html__( 'AMOUNT PAID', 'woocommerce-payments' ); ?></b>:</td> <td class="align-right"><b><?php echo wp_kses( wc_price( $amount_captured, [ 'currency' => $order['currency'] ] ), 'post' ); ?></b></td> </tr> <tr> <td colspan="2" class="align-left"><?php echo esc_html( sprintf( '%s - %s', ucfirst( $payment_method_details['brand'] ), $payment_method_details['last4'] ) ); ?></td> </tr> </table> </div> <hr /> <div class="receipt__transaction"> <p id="application-preferred-name"><?php printf( '%s: %s', esc_html__( 'Application name', 'woocommerce-payments' ), esc_html( ucfirst( $receipt['application_preferred_name'] ) ) ); ?></p> <p id="dedicated-file-name"><?php printf( '%s: %s', esc_html__( 'AID', 'woocommerce-payments' ), esc_html( ucfirst( $receipt['dedicated_file_name'] ) ) ); ?></p> <p id="account_type"><?php printf( '%s: %s', esc_html__( 'Account Type', 'woocommerce-payments' ), esc_html( ucfirst( $receipt['account_type'] ) ) ); ?></p> <p id="powered_by"><?php echo esc_html__( 'Powered by WooCommerce', 'woocommerce-payments' ); ?></p> </div> </div> </body> </html> includes/in-person-payments/class-wc-payments-in-person-payments-receipts-service.php 0000777 00000012621 15251156605 0025244 0 ustar 00 <?php /** * Class WC_Payments_In_Person_Payment_Print_Receipt_Service * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; /** * Class handling in person payments receipts. */ class WC_Payments_In_Person_Payments_Receipts_Service { /** * Renders the receipt template. * * @param array $settings Merchant settings. * @param WC_Order $order Order instance. * @param array $charge Charge data. * * @return string */ public function get_receipt_markup( array $settings, WC_Order $order, array $charge ): string { $this->validate_settings( $settings ); $this->validate_charge( $charge ); if ( $order instanceof WC_Payments_Printed_Receipt_Sample_Order ) { $order_data = $order->get_data(); $line_items_data = $order_data['line_items']; } else { $order_data = [ 'id' => $order->get_id(), 'currency' => $order->get_currency(), 'subtotal' => $order->get_subtotal(), 'line_items' => $order->get_items(), 'coupon_lines' => $order->get_items( 'coupon' ), 'tax_lines' => $order->get_items( 'tax' ), 'total' => $order->get_total(), 'shipping_tax' => $order->get_shipping_methods() ? $order->get_shipping_total() : 0, 'total_fees' => $order->get_total_fees(), ]; $line_items_data = $this->format_line_items( $order_data ); } ob_start(); wc_get_template( 'html-in-person-payment-receipt.php', [ 'amount_captured' => $charge['amount_captured'] / 100, 'coupon_lines' => $order_data['coupon_lines'] ?? [], 'branding_logo' => $settings['branding_logo'] ?? [], 'business_name' => $settings['business_name'], 'line_items' => $line_items_data, 'order' => $order_data, 'payment_method_details' => $charge['payment_method_details']['card_present'], 'receipt' => $charge['payment_method_details']['card_present']['receipt'], 'support_address' => $settings['support_info']['address'], 'support_email' => $settings['support_info']['email'], 'support_phone' => $settings['support_info']['phone'], 'tax_lines' => $order_data['tax_lines'] ?? [], ], '', __DIR__ . '/templates/' ); return ob_get_clean(); } /** * Send card reader receipt to customer by email * * @param WC_Order $order the order. * @param array $merchant_settings The merchant settings. * @param array $charge the charge. * @return void */ public function send_customer_ipp_receipt_email( WC_Order $order, array $merchant_settings, array $charge ) { $email_receipt = WC()->mailer()->get_emails()['WC_Payments_Email_IPP_Receipt']; if ( $email_receipt instanceof WC_Payments_Email_IPP_Receipt ) { $email_receipt->trigger( $order, $merchant_settings, $charge ); } } /** * Format line items * * @param array $order the order. * @return array */ private function format_line_items( array $order ): array { $line_items_data = []; foreach ( $order['line_items'] as $item ) { $item_data = $item->get_data(); $item_data['product'] = $item->get_product()->get_data(); $line_items_data[] = $item_data; } return $line_items_data; } /** * Validate settings * * @param array $settings Settings. * @return void * @throws \RuntimeException Error validating settings. */ private function validate_settings( array $settings ) { if ( ! array_key_exists( 'business_name', $settings ) ) { throw new \RuntimeException( 'Business name needs to be provided.' ); } if ( empty( $settings['support_info'] ) || ! is_array( $settings['support_info'] ) ) { throw new \RuntimeException( 'Support information needs to be provided.' ); } $this->validate_required_fields( [ 'address', 'email', 'phone' ], $settings['support_info'], 'Error validating support information' ); } /** * Validate charge information * * @param array $charge Charge info. * @return void * @throws \RuntimeException Error validating charge info. */ private function validate_charge( array $charge ) { if ( ! array_key_exists( 'amount_captured', $charge ) ) { throw new \RuntimeException( 'Captured amount needs to be provided.' ); } if ( empty( $charge['payment_method_details']['card_present'] ) || ! is_array( $charge['payment_method_details']['card_present'] ) ) { throw new \RuntimeException( 'Payment method details needs to be provided.' ); } $this->validate_required_fields( [ 'brand', 'last4', 'receipt' ], $charge['payment_method_details']['card_present'], 'Error validating payment information' ); $this->validate_required_fields( [ 'application_preferred_name', 'dedicated_file_name', 'account_type' ], $charge['payment_method_details']['card_present']['receipt'], 'Error validating receipt information' ); } /** * Validate required field * * @param array $required_fields Required fields. * @param array $data Data to validate. * @param string $message Error message. * @return void * @throws \RuntimeException Error validating required fields. */ private function validate_required_fields( array $required_fields, array $data, string $message ) { foreach ( $required_fields as $required_key ) { if ( ! array_key_exists( $required_key, $data ) ) { throw new \RuntimeException( esc_html( sprintf( '%s. Missing key: %s', $message, $required_key ) ) ); } } } } includes/class-wc-payments-features.php 0000777 00000034127 15251156605 0014275 0 ustar 00 <?php /** * Class WC_Payments_Features * * @package WooCommerce\Payments */ use WCPay\Constants\Country_Code; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WC Payments Features class */ class WC_Payments_Features { /** * If you need to remove or deprecate a flag: * - Please update the `Erase_Deprecated_Flags_And_Options` migration with: * - The next version of WooPayments. * - The flag to be deleted. */ const WCPAY_SUBSCRIPTIONS_FLAG_NAME = '_wcpay_feature_subscriptions'; const STRIPE_BILLING_FLAG_NAME = '_wcpay_feature_stripe_billing'; const WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME = '_wcpay_feature_woopay_express_checkout'; const WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME = '_wcpay_feature_woopay_first_party_auth'; const WOOPAY_DIRECT_CHECKOUT_FLAG_NAME = '_wcpay_feature_woopay_direct_checkout'; const DISPUTE_ISSUER_EVIDENCE = '_wcpay_feature_dispute_issuer_evidence'; const DISPUTE_ADDITIONAL_EVIDENCE_TYPES = '_wcpay_feature_dispute_additional_evidence_types'; const WOOPAY_GLOBAL_THEME_SUPPORT_FLAG_NAME = '_wcpay_feature_woopay_global_theme_support'; const WCPAY_DYNAMIC_CHECKOUT_PLACE_ORDER_BUTTON_FLAG_NAME = '_wcpay_feature_dynamic_checkout_place_order_button'; const AMAZON_PAY_FLAG_NAME = '_wcpay_feature_amazon_pay'; /** * Indicates whether card payments are enabled for this (Stripe) account. * * @return bool True if account can accept card payments, false otherwise. */ public static function are_payments_enabled() { $account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); return is_array( $account ) && ( $account['payments_enabled'] ?? false ); } /** * Checks if WooPay is enabled. * * @return bool */ public static function is_woopay_enabled() { $is_woopay_eligible = self::is_woopay_eligible(); // Feature flag. $is_woopay_enabled = 'yes' === WC_Payments::get_gateway()->get_option( 'platform_checkout' ); $is_woopay_express_button_enabled = self::is_woopay_express_checkout_enabled(); return $is_woopay_eligible && $is_woopay_enabled && $is_woopay_express_button_enabled; } /** * Checks whether the customer Multi-Currency feature is enabled * * @return bool */ public static function is_customer_multi_currency_enabled() { return '1' === get_option( '_wcpay_feature_customer_multi_currency', '1' ); } /** * Checks whether WCPay Subscriptions is enabled. * * @return bool */ public static function is_wcpay_subscriptions_enabled() { // After completing the WooCommerce onboarding, check if the merchant has chosen Subscription product types and enable the feature flag. if ( (bool) get_option( 'wcpay_check_subscriptions_eligibility_after_onboarding', false ) ) { if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '7.9.0', '<' ) ) { self::maybe_enable_wcpay_subscriptions_after_onboarding( [], get_option( 'woocommerce_onboarding_profile', [] ) ); } delete_option( 'wcpay_check_subscriptions_eligibility_after_onboarding' ); } return apply_filters( 'wcpay_is_wcpay_subscriptions_enabled', '1' === get_option( self::WCPAY_SUBSCRIPTIONS_FLAG_NAME, '0' ) ); } /** * Returns whether the store is eligible to use WCPay Subscriptions (the free subscriptions bundled in WooPayments) * * Stores are eligible for the WCPay Subscriptions feature if: * 1. The store has existing WCPay Subscriptions, or * 2. The store has Stripe Billing product metadata on at least 1 product subscription product. * * @return bool */ public static function is_wcpay_subscriptions_eligible() { /** * Check if they have at least 1 WCPay Subscription. * * Note: this is only possible if WCPay Subscriptions is enabled, otherwise the wcs_get_subscriptions function wouldn't exist. */ if ( function_exists( 'wcs_get_subscriptions' ) ) { $wcpay_subscriptions = wcs_get_subscriptions( [ 'subscriptions_per_page' => 1, 'subscription_status' => 'any', 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query [ 'key' => '_wcpay_subscription_id', 'compare' => 'EXISTS', ], ], ] ); if ( ( is_countable( $wcpay_subscriptions ) ? count( $wcpay_subscriptions ) : 0 ) > 0 ) { return true; } } /** * Check if they have at least 1 Stripe Billing enabled product. */ $stripe_billing_meta_query_handler = function ( $query, $query_vars ) { if ( ! empty( $query_vars['stripe_billing_product'] ) ) { $query['meta_query'][] = [ 'key' => '_wcpay_product_hash', 'compare' => 'EXISTS', ]; } return $query; }; add_filter( 'woocommerce_product_data_store_cpt_get_products_query', $stripe_billing_meta_query_handler, 10, 2 ); $subscription_products = wc_get_products( [ 'limit' => 1, 'type' => [ 'subscription', 'variable-subscription' ], 'status' => 'publish', 'return' => 'ids', 'stripe_billing_product' => 'true', ] ); remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', $stripe_billing_meta_query_handler, 10, 2 ); if ( ( is_countable( $subscription_products ) ? count( $subscription_products ) : 0 ) > 0 ) { return true; } return false; } /** * Checks whether the merchant has chosen Subscription product types during onboarding * WooCommerce and is elible for WCPay Subscriptions, if so, enables the feature flag. * * @since 6.2.0 * * @param array $onboarding_data Onboarding data. * @param array $updated Updated onboarding settings. * * @return void */ public static function maybe_enable_wcpay_subscriptions_after_onboarding( $onboarding_data, $updated ) { if ( empty( $updated['product_types'] ) || ! is_array( $updated['product_types'] ) || ! in_array( 'subscriptions', $updated['product_types'], true ) ) { return; } if ( ! self::is_wcpay_subscriptions_eligible() ) { return; } update_option( self::WCPAY_SUBSCRIPTIONS_FLAG_NAME, '1' ); } /** * Checks whether woopay is enabled. * * @return bool */ public static function is_woopay_eligible() { // Checks for the dependency on Store API AbstractCartRoute. if ( ! class_exists( 'Automattic\WooCommerce\StoreApi\Routes\V1\AbstractCartRoute' ) ) { return false; } // read directly from cache, ignore cache expiration check. $account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); $is_account_rejected = WC_Payments::get_account_service()->is_account_rejected(); $is_account_under_review = WC_Payments::get_account_service()->is_account_under_review(); return is_array( $account ) && ( $account['platform_checkout_eligible'] ?? false ) && ! $is_account_rejected && ! $is_account_under_review; } /** * Checks whether documents section is enabled. * * @return bool */ public static function is_documents_section_enabled() { $account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); return is_array( $account ) && ( $account['is_documents_enabled'] ?? false ); } /** * Checks whether WooPay Express Checkout is enabled. * * @return bool */ public static function is_woopay_express_checkout_enabled() { // Confirm woopay eligibility as well. return '1' === get_option( self::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ) && self::is_woopay_eligible(); } /** * Checks whether WooPay First Party Auth is enabled. * * @return bool */ public static function is_woopay_first_party_auth_enabled() { return '1' === get_option( self::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '1' ) && self::is_woopay_express_checkout_enabled(); } /** * Checks whether WooPay Direct Checkout is enabled. * * @return bool True if Direct Checkout is enabled, false otherwise. */ public static function is_woopay_direct_checkout_enabled() { $account_cache = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); $is_direct_checkout_eligible = is_array( $account_cache ) && ( $account_cache['platform_direct_checkout_eligible'] ?? false ); $is_direct_checkout_flag_enabled = '1' === get_option( self::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' ); return $is_direct_checkout_eligible && $is_direct_checkout_flag_enabled && self::is_woopayments_gateway_enabled() && self::is_woopay_enabled(); } /** * Checks whether WooPay global theme support is eligible. * * @return bool */ public static function is_woopay_global_theme_support_eligible() { $account_cache = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); return is_array( $account_cache ) && ( $account_cache['platform_global_theme_support_enabled'] ?? false ); } /** * Checks whether the Fraud and Risk Tools feature flag is enabled. * * @return bool */ public static function is_frt_review_feature_active(): bool { return '1' === get_option( 'wcpay_frt_review_feature_active', '0' ); } /** * Checks whether the Fraud and Risk Tools welcome tour was dismissed. * * @return bool */ public static function is_fraud_protection_welcome_tour_dismissed(): bool { return '1' === get_option( 'wcpay_fraud_protection_welcome_tour_dismissed', '0' ); } /** * Checks whether the Stripe Billing feature is enabled. * * @return bool */ public static function is_stripe_billing_enabled(): bool { return '1' === get_option( self::STRIPE_BILLING_FLAG_NAME, '0' ); } /** * Checks if the site is eligible for Stripe Billing. * * Only US merchants are eligible for Stripe Billing. * * @return bool */ public static function is_stripe_billing_eligible() { if ( ! function_exists( 'wc_get_base_location' ) ) { return false; } $store_base_location = wc_get_base_location(); return ! empty( $store_base_location['country'] ) && Country_Code::UNITED_STATES === $store_base_location['country']; } /** * Checks whether the merchant is using WCPay Subscription or opted into Stripe Billing. * * Note: Stripe Billing is only used when the merchant is using WooCommerce Subscriptions and turned it on or is still using WCPay Subscriptions. * * @return bool */ public static function should_use_stripe_billing() { // We intentionally check for the existence of the 'WC_Subscriptions' class here as we want to confirm the Plugin is active. if ( self::is_wcpay_subscriptions_enabled() && ! class_exists( 'WC_Subscriptions' ) ) { return true; } if ( self::is_stripe_billing_enabled() && class_exists( 'WC_Subscriptions' ) ) { return true; } return false; } /** * Checks whether Dispute issuer evidence feature should be enabled. Disabled by default. * * @return bool */ public static function is_dispute_issuer_evidence_enabled(): bool { return '1' === get_option( self::DISPUTE_ISSUER_EVIDENCE, '0' ); } /** * Checks whether Dispute Additional Evidence Types feature should be enabled. Disabled by default. * * This gates the new evidence form types (event, booking_reservation, other) for dispute challenges. * * @return bool */ public static function is_dispute_additional_evidence_types_enabled(): bool { return '1' === get_option( self::DISPUTE_ADDITIONAL_EVIDENCE_TYPES, '0' ); } /** * Checks whether the next deposit notice on the deposits list screen has been dismissed. * * @return bool */ public static function is_next_deposit_notice_dismissed(): bool { return '1' === get_option( 'wcpay_next_deposit_notice_dismissed', '0' ); } /** * Checks whether the dynamic checkout place order button feature is enabled. * * @return bool */ public static function is_dynamic_checkout_place_order_button_enabled(): bool { return '1' === get_option( self::WCPAY_DYNAMIC_CHECKOUT_PLACE_ORDER_BUTTON_FLAG_NAME, '0' ); } /** * Checks whether Amazon Pay is enabled. * * @return bool */ public static function is_amazon_pay_enabled(): bool { return '1' === get_option( self::AMAZON_PAY_FLAG_NAME, '0' ) && self::is_ece_confirmation_tokens_enabled(); } /** * Checks whether ECE should use confirmation tokens instead of payment methods. * * @see https://docs.stripe.com/payments/finalize-payments-on-the-server-migration * * @return bool */ public static function is_ece_confirmation_tokens_enabled(): bool { $account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); return is_array( $account ) && ! ( $account['ece_confirmation_tokens_disabled'] ?? false ); } /** * Returns feature flags as an array suitable for display on the front-end. * * @return bool[] */ public static function to_array() { return array_filter( [ 'multiCurrency' => self::is_customer_multi_currency_enabled(), 'woopay' => self::is_woopay_eligible(), 'documents' => self::is_documents_section_enabled(), 'woopayExpressCheckout' => self::is_woopay_express_checkout_enabled(), 'isDisputeIssuerEvidenceEnabled' => self::is_dispute_issuer_evidence_enabled(), 'isDisputeAdditionalEvidenceTypesEnabled' => self::is_dispute_additional_evidence_types_enabled(), 'isFRTReviewFeatureActive' => self::is_frt_review_feature_active(), 'isDynamicCheckoutPlaceOrderButtonEnabled' => self::is_dynamic_checkout_place_order_button_enabled(), 'amazonPay' => self::is_amazon_pay_enabled(), 'isEceUsingConfirmationTokens' => self::is_ece_confirmation_tokens_enabled(), ] ); } /** * Checks if WooCommerce Payments gateway is enabled. * * @return bool True if WooCommerce Payments gateway is enabled, false otherwise. */ private static function is_woopayments_gateway_enabled() { $woopayments_settings = get_option( 'woocommerce_woocommerce_payments_settings' ); $woopayments_enabled_setting = $woopayments_settings['enabled'] ?? 'no'; return 'yes' === $woopayments_enabled_setting; } } includes/class-wc-payments-webhook-processing-service.php 0000777 00000106262 15251156605 0017725 0 ustar 00 <?php /** * WC_Payments_Webhook_Processing_Service class * * @package WooCommerce\Payments */ use WCPay\Constants\Order_Status; use WCPay\Constants\Payment_Method; use WCPay\Core\Server\Request\Get_Intention; use WCPay\Database_Cache; use WCPay\Exceptions\Invalid_Payment_Method_Exception; use WCPay\Exceptions\Invalid_Webhook_Data_Exception; use WCPay\Exceptions\Order_Not_Found_Exception; use WCPay\Exceptions\Rest_Request_Exception; use WCPay\Logger; use WCPay\Constants\Refund_Status; use WCPay\Constants\Refund_Failure_Reason; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Service to process webhook data. */ class WC_Payments_Webhook_Processing_Service { /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ protected $api_client; /** * DB wrapper. * * @var WC_Payments_DB */ private $wcpay_db; /** * WC Payments Account. * * @var WC_Payments_Account */ private $account; /** * WC Payments Remote Note Service. * * @var WC_Payments_Remote_Note_Service */ private $remote_note_service; /** * WC_Payments_Order_Service instance * * @var WC_Payments_Order_Service */ protected $order_service; /** * WC_Payments_In_Person_Payments_Receipts_Service * * @var WC_Payments_In_Person_Payments_Receipts_Service */ private $receipt_service; /** * WC_Payment_Gateway_WCPay * * @var WC_Payment_Gateway_WCPay */ private $wcpay_gateway; /** * Database_Cache instance. * * @var Database_Cache */ private $database_cache; /** * WC_Payments_Onboarding_Service instance. * * @var WC_Payments_Onboarding_Service */ private $onboarding_service; /** * WC_Payments_Token_Service instance. * * @var WC_Payments_Token_Service */ private $token_service; /** * WC_Payments_Webhook_Processing_Service constructor. * * @param WC_Payments_API_Client $api_client WooCommerce Payments API client. * @param WC_Payments_DB $wcpay_db WC_Payments_DB instance. * @param WC_Payments_Account $account WC_Payments_Account instance. * @param WC_Payments_Remote_Note_Service $remote_note_service WC_Payments_Remote_Note_Service instance. * @param WC_Payments_Order_Service $order_service WC_Payments_Order_Service instance. * @param WC_Payments_In_Person_Payments_Receipts_Service $receipt_service WC_Payments_In_Person_Payments_Receipts_Service instance. * @param WC_Payment_Gateway_WCPay $wcpay_gateway WC_Payment_Gateway_WCPay instance. * @param Database_Cache $database_cache Database_Cache instance. * @param WC_Payments_Onboarding_Service $onboarding_service WC_Payments_Onboarding_Service instance. * @param WC_Payments_Token_Service $token_service WC_Payments_Token_Service instance. */ public function __construct( WC_Payments_API_Client $api_client, WC_Payments_DB $wcpay_db, WC_Payments_Account $account, WC_Payments_Remote_Note_Service $remote_note_service, WC_Payments_Order_Service $order_service, WC_Payments_In_Person_Payments_Receipts_Service $receipt_service, WC_Payment_Gateway_WCPay $wcpay_gateway, Database_Cache $database_cache, WC_Payments_Onboarding_Service $onboarding_service, WC_Payments_Token_Service $token_service ) { $this->wcpay_db = $wcpay_db; $this->account = $account; $this->remote_note_service = $remote_note_service; $this->order_service = $order_service; $this->api_client = $api_client; $this->receipt_service = $receipt_service; $this->wcpay_gateway = $wcpay_gateway; $this->database_cache = $database_cache; $this->onboarding_service = $onboarding_service; $this->token_service = $token_service; } /** * Process webhook event data. * * @param array $event_body Body data of webhook request. * * @return void * * @throws Invalid_Webhook_Data_Exception */ public function process( array $event_body ) { // Extract information about the webhook event. $event_type = $this->read_webhook_property( $event_body, 'type' ); $event_id = ''; try { $event_id = $this->read_webhook_property( $event_body, 'id' ); } catch ( Invalid_Webhook_Data_Exception $e ) { Logger::error( 'Webhook event ID not found' ); } Logger::debug( 'WEBHOOK RECEIVED: ' . $event_type . ' ' . $event_id, [ 'body' => WC_Payments_Utils::redact_array( $event_body, WC_Payments_API_Client::API_KEYS_TO_REDACT ), ] ); if ( $this->is_webhook_mode_mismatch( $event_body ) ) { return; } try { do_action( 'woocommerce_payments_before_webhook_delivery', $event_type, $event_body ); } catch ( Exception $e ) { Logger::error( $e ); } switch ( $event_type ) { case 'charge.refunded': $this->process_webhook_refund_triggered_externally( $event_body ); break; case 'charge.refund.updated': $this->process_webhook_refund_updated( $event_body ); break; case 'charge.dispute.created': $this->process_webhook_dispute_created( $event_body ); break; case 'charge.dispute.closed': $this->process_webhook_dispute_closed( $event_body ); break; case 'charge.dispute.funds_reinstated': case 'charge.dispute.funds_withdrawn': case 'charge.dispute.updated': $this->process_webhook_dispute_updated( $event_body ); break; case 'charge.expired': $this->process_webhook_expired_authorization( $event_body ); break; case 'account.updated': $this->account->refresh_account_data(); $this->token_service->clear_all_cached_payment_methods(); break; case 'account.deleted': $this->onboarding_service->cleanup_on_account_reset(); // Reset the WooCommerce NOX data, if it is not already. delete_option( WC_Payments_Account::NOX_PROFILE_OPTION_KEY ); // NOX onboarding should be unlocked by the time we receive this event, // but unlock it just in case, to maintain sanity. delete_option( WC_Payments_Account::NOX_ONBOARDING_LOCKED_KEY ); // Refetch the account data to allow the platform to drive the available next steps. $this->account->refresh_account_data(); // Use the opportunity to clear cached payment methods. $this->token_service->clear_all_cached_payment_methods(); break; case 'wcpay.notification': $this->process_wcpay_notification( $event_body ); break; case 'payment_intent.payment_failed': $this->process_webhook_payment_intent_failed( $event_body ); break; case 'payment_intent.succeeded': $this->process_webhook_payment_intent_succeeded( $event_body ); break; case 'payment_intent.canceled': $this->process_webhook_payment_intent_canceled( $event_body ); break; case 'payment_intent.amount_capturable_updated': $this->process_webhook_payment_intent_amount_capturable_updated( $event_body ); break; case 'invoice.upcoming': case 'invoice.paid': case 'invoice.payment_failed': $this->process_webhook_stripe_billing_invoice( $event_type, $event_body ); break; } try { do_action( 'woocommerce_payments_after_webhook_delivery', $event_type, $event_body ); } catch ( Exception $e ) { Logger::error( $e ); } } /** * Check webhook mode against the gateway mode. * * @param array $event_body The event that triggered the webhook. * * @return bool Indicates whether the event's mode is different from the gateway's mode * @throws Invalid_Webhook_Data_Exception Event mode does not match the gateway mode. */ private function is_webhook_mode_mismatch( array $event_body ): bool { if ( ! $this->has_webhook_property( $event_body, 'livemode' ) ) { return false; } $is_gateway_live_mode = WC_Payments::mode()->is_live(); $is_event_live_mode = $this->read_webhook_property( $event_body, 'livemode' ); if ( $is_gateway_live_mode !== $is_event_live_mode ) { $event_id = $this->read_webhook_property( $event_body, 'id' ); Logger::error( sprintf( 'Webhook event mode did not match the gateway mode (event ID: %s)', $event_id ) ); return true; } return false; } /** * Process webhook refund updated. * * @param array $event_body The event that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. * @throws Invalid_Payment_Method_Exception When unable to resolve charge ID to order. */ private function process_webhook_refund_updated( $event_body ) { $event_data = $this->read_webhook_property( $event_body, 'data' ); $event_object = $this->read_webhook_property( $event_data, 'object' ); // Fetch the details of the failed refund so that we can find the associated order and write a note. $charge_id = $this->read_webhook_property( $event_object, 'charge' ); $refund_id = $this->read_webhook_property( $event_object, 'id' ); $amount = $this->read_webhook_property( $event_object, 'amount' ); $currency = $this->read_webhook_property( $event_object, 'currency' ); $status = $this->read_webhook_property( $event_object, 'status' ); $balance_transaction = $this->has_webhook_property( $event_object, 'balance_transaction' ) ? $this->read_webhook_property( $event_object, 'balance_transaction' ) : null; // Look up the order related to this charge. $order = $this->wcpay_db->order_from_charge_id( $charge_id ); if ( ! $order ) { throw new Invalid_Payment_Method_Exception( sprintf( /* translators: %1: charge ID */ __( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ), $charge_id ), 'order_not_found' ); } $matched_wc_refund = null; /** * Get the WC_Refund from the WCPay refund ID. * * @var WC_Order_Refund[] $wc_refunds * */ $wc_refunds = $order->get_refunds(); if ( ! empty( $wc_refunds ) ) { foreach ( $wc_refunds as $wc_refund ) { $wcpay_refund_id = $this->order_service->get_wcpay_refund_id_for_order( $wc_refund ); if ( $refund_id === $wcpay_refund_id ) { $matched_wc_refund = $wc_refund; break; } } } // Refund update webhook events can be either failed, cancelled (basically it's also a failure but triggered by the merchant), succeeded only. switch ( $status ) { case Refund_Status::FAILED: $failure_reason = $this->has_webhook_property( $event_object, 'failure_reason' ) ? $this->read_webhook_property( $event_object, 'failure_reason' ) : null; $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, $matched_wc_refund, false, $failure_reason ); break; case Refund_Status::CANCELED: $this->order_service->handle_failed_refund( $order, $refund_id, $amount, $currency, $matched_wc_refund, true ); break; case Refund_Status::SUCCEEDED: if ( $matched_wc_refund ) { $this->order_service->add_note_and_metadata_for_created_refund( $order, $matched_wc_refund, $refund_id, $balance_transaction ?? null ); } break; default: throw new Invalid_Webhook_Data_Exception( 'Invalid refund update status: ' . $status ); } } /** * Process webhook for an expired uncaptured payment. * * @param array $event_body The event that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. * @throws Invalid_Payment_Method_Exception When unable to resolve charge ID to order. */ private function process_webhook_expired_authorization( $event_body ) { $event_data = $this->read_webhook_property( $event_body, 'data' ); $event_object = $this->read_webhook_property( $event_data, 'object' ); // Fetch the details of the expired auth so that we can find the associated order. $charge_id = $this->read_webhook_property( $event_object, 'id' ); // Look up the order related to this charge. $order = $this->wcpay_db->order_from_charge_id( $charge_id ); if ( ! $order ) { throw new Invalid_Payment_Method_Exception( sprintf( /* translators: %1: charge ID */ __( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ), $charge_id ), 'order_not_found' ); } // Get the intent_id and then its status. $intent_id = $event_object['payment_intent'] ?? $order->get_meta( '_intent_id' ); $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); $intent = $request->send(); $intent_status = $intent->get_status(); // TODO: Revisit this logic once we support partial captures or multiple charges for order. We'll need to handle the "payment_intent.canceled" event too. $this->order_service->mark_payment_capture_expired( $order, $intent_id, $intent_status, $charge_id ); // Clear the authorization summary cache to trigger a fetch of new data. $this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY ); $this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY_TEST_MODE ); } /** * Process webhook for a payment intent canceled event. * * @param array $event_body The event that triggered the webhook. * * @return void */ private function process_webhook_payment_intent_canceled( $event_body ) { // Clear the authorization summary cache to trigger a fetch of new data. $this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY ); $this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY_TEST_MODE ); } /** * Process webhook for a payment intent amount capturable updated event. * * @param array $event_body The event that triggered the webhook. * * @return void */ private function process_webhook_payment_intent_amount_capturable_updated( $event_body ) { // Clear the authorization summary cache to trigger a fetch of new data. $this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY ); $this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY_TEST_MODE ); } /** * Process webhook for a failed payment intent. * * @param array $event_body The event that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. * @throws Invalid_Payment_Method_Exception When unable to resolve charge ID to order. */ private function process_webhook_payment_intent_failed( $event_body ) { // Check to make sure we should process this according to the payment method. $charge_id = $event_body['data']['object']['charges']['data'][0]['id'] ?? ''; $last_payment_error = $event_body['data']['object']['last_payment_error'] ?? null; $payment_method = $last_payment_error['payment_method'] ?? null; $payment_method_type = $payment_method['type'] ?? null; $actionable_methods = [ Payment_Method::CARD, Payment_Method::CARD_PRESENT, Payment_Method::US_BANK_ACCOUNT, Payment_Method::BECS, Payment_Method::WECHAT_PAY, ]; if ( empty( $payment_method_type ) || ! in_array( $payment_method_type, $actionable_methods, true ) ) { return; } // Get the order and make sure it is an order and the payment methods match. $order = $this->get_order_from_event_body( $event_body ); $payment_method_id = $payment_method['id'] ?? null; if ( ! $order || empty( $payment_method_id ) ) { return; } if ( Payment_Method::CARD_PRESENT !== $payment_method_type && $payment_method_id !== $order->get_meta( '_payment_method_id' ) ) { return; } $event_data = $this->read_webhook_property( $event_body, 'data' ); $event_object = $this->read_webhook_property( $event_data, 'object' ); $intent_id = $this->read_webhook_property( $event_object, 'id' ); $intent_status = $this->read_webhook_property( $event_object, 'status' ); if ( Payment_Method::CARD_PRESENT === $payment_method_type ) { $this->order_service->mark_terminal_payment_failed( $order, $intent_id, $intent_status, $charge_id, $this->get_failure_message_from_error( $last_payment_error ) ); } else { $this->order_service->mark_payment_failed( $order, $intent_id, $intent_status, $charge_id, $this->get_failure_message_from_error( $last_payment_error ) ); } } /** * Process webhook for a successful payment intent. * * @param array $event_body The event that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. * @throws Invalid_Payment_Method_Exception When unable to resolve intent ID to order. */ private function process_webhook_payment_intent_succeeded( $event_body ) { $event_data = $this->read_webhook_property( $event_body, 'data' ); $event_object = $this->read_webhook_property( $event_data, 'object' ); $intent_id = $this->read_webhook_property( $event_object, 'id' ); $currency = $this->read_webhook_property( $event_object, 'currency' ); $order = $this->get_order_from_event_body( $event_body ); $event_charges = $this->read_webhook_property( $event_object, 'charges' ); $charges_data = $this->read_webhook_property( $event_charges, 'data' ); $charge_id = $this->read_webhook_property( $charges_data[0], 'id' ); $charge_amount = $this->read_webhook_property( $event_object, 'amount' ); $payment_method_id = $charges_data[0]['payment_method'] ?? null; if ( ! $order ) { return; } // Update missing intents because webhook can be delivered before order is processed on the client. $meta_data_to_update = [ '_intent_id' => $intent_id, '_charge_id' => $charge_id, '_payment_method_id' => $payment_method_id, WC_Payments_Utils::ORDER_INTENT_CURRENCY_META_KEY => $currency, ]; // Save mandate id, necessary for some subscription renewals. $mandate_id = $event_data['object']['charges']['data'][0]['payment_method_details']['card']['mandate'] ?? null; if ( $mandate_id ) { $meta_data_to_update['_stripe_mandate_id'] = $mandate_id; } $application_fee_amount = $charges_data[0]['application_fee_amount'] ?? null; if ( $application_fee_amount ) { $fee = WC_Payments_Utils::interpret_stripe_amount( $application_fee_amount, $currency ); $meta_data_to_update['_wcpay_transaction_fee'] = $fee; $charge_amount = WC_Payments_Utils::interpret_stripe_amount( $charge_amount, $currency ); $meta_data_to_update['_wcpay_net'] = $charge_amount - $fee; } foreach ( $meta_data_to_update as $key => $value ) { // Override existing meta data with incoming values, if present. if ( $value ) { $order->update_meta_data( $key, $value ); } } // Save the order after updating the meta data values. $order->save(); // This is an incoming request from WCPay server rather than an outgoing request to WCPay server. // However, the shape of the payment intent object are the same. // Using this extraction method will reduce the code duplication. $payment_intent = $this->api_client->deserialize_payment_intention_object_from_array( $event_object ); $this->order_service->update_order_status_from_intent( $order, $payment_intent ); $payment_method = $charges_data[0]['payment_method_details']['type'] ?? null; // Send the customer a card reader receipt if it's an in person payment type. if ( Payment_Method::CARD_PRESENT === $payment_method || Payment_Method::INTERAC_PRESENT === $payment_method ) { $merchant_settings = [ 'business_name' => $this->wcpay_gateway->get_option( 'account_business_name' ), 'support_info' => [ 'address' => $this->wcpay_gateway->get_option( 'account_business_support_address' ), 'phone' => $this->wcpay_gateway->get_option( 'account_business_support_phone' ), 'email' => $this->wcpay_gateway->get_option( 'account_business_support_email' ), ], ]; $this->receipt_service->send_customer_ipp_receipt_email( $order, $merchant_settings, $charges_data[0] ); } // Clear the authorization summary cache to trigger a fetch of new data. $this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY ); $this->database_cache->delete( DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY_TEST_MODE ); } /** * Process webhook dispute created. * * @param array $event_body The event that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. */ private function process_webhook_dispute_created( $event_body ) { $event_data = $this->read_webhook_property( $event_body, 'data' ); $event_object = $this->read_webhook_property( $event_data, 'object' ); $charge_id = $this->read_webhook_property( $event_object, 'charge' ); $reason = $this->read_webhook_property( $event_object, 'reason' ); $amount_raw = $this->read_webhook_property( $event_object, 'amount' ); $evidence = $this->read_webhook_property( $event_object, 'evidence_details' ); $status = $this->read_webhook_property( $event_object, 'status' ); $due_by = $this->read_webhook_property( $evidence, 'due_by' ); $order = $this->wcpay_db->order_from_charge_id( $charge_id ); $currency = $order->get_currency(); $amount_string = wc_price( WC_Payments_Utils::interpret_stripe_amount( $amount_raw, $currency ), [ 'currency' => strtoupper( $currency ) ] ); // Explicitly add currency info if needed (multi-currency stores). $amount = WC_Payments_Explicit_Price_Formatter::get_explicit_price_with_currency( $amount_string, $currency ); // Convert due_by to a date string in the store timezone. $due_by = date_i18n( wc_date_format(), $due_by ); if ( ! $order ) { throw new Invalid_Webhook_Data_Exception( sprintf( /* translators: %1: charge ID */ __( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ), $charge_id ) ); } $this->order_service->mark_payment_dispute_created( $order, $charge_id, $amount, $reason, $due_by, $status ); // Clear dispute caches to trigger a fetch of new data. $this->database_cache->delete_dispute_caches(); } /** * Process webhook dispute closed. * * @param array $event_body The event that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. */ private function process_webhook_dispute_closed( $event_body ) { $event_data = $this->read_webhook_property( $event_body, 'data' ); $event_object = $this->read_webhook_property( $event_data, 'object' ); $charge_id = $this->read_webhook_property( $event_object, 'charge' ); $status = $this->read_webhook_property( $event_object, 'status' ); $order = $this->wcpay_db->order_from_charge_id( $charge_id ); if ( ! $order ) { throw new Invalid_Webhook_Data_Exception( sprintf( /* translators: %1: charge ID */ __( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ), $charge_id ) ); } $this->order_service->mark_payment_dispute_closed( $order, $charge_id, $status ); // Clear dispute caches to trigger a fetch of new data. $this->database_cache->delete_dispute_caches(); } /** * Process webhook dispute updated. * * @param array $event_body The event that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. */ private function process_webhook_dispute_updated( $event_body ) { $event_type = $this->read_webhook_property( $event_body, 'type' ); $event_data = $this->read_webhook_property( $event_body, 'data' ); $event_object = $this->read_webhook_property( $event_data, 'object' ); $charge_id = $this->read_webhook_property( $event_object, 'charge' ); $order = $this->wcpay_db->order_from_charge_id( $charge_id ); if ( ! $order ) { throw new Invalid_Webhook_Data_Exception( sprintf( /* translators: %1: charge ID */ __( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ), $charge_id ) ); } switch ( $event_type ) { case 'charge.dispute.funds_withdrawn': $message = __( 'Payment dispute and fees have been deducted from your next payout', 'woocommerce-payments' ); break; case 'charge.dispute.funds_reinstated': $message = __( 'Payment dispute funds have been reinstated', 'woocommerce-payments' ); break; default: $message = __( 'Payment dispute has been updated', 'woocommerce-payments' ); } $note = sprintf( /* translators: %1: the dispute message, %2: the dispute details URL */ __( '%1$s. See <a href="%2$s">dispute overview</a> for more details.', 'woocommerce-payments' ), $message, add_query_arg( [ 'id' => $charge_id ], admin_url( 'admin.php?page=wc-admin&path=/payments/transactions/details' ) ) ); if ( $this->order_service->order_note_exists( $order, $note ) ) { return; } $order->add_order_note( $note ); // Clear dispute caches to trigger a fetch of new data. $this->database_cache->delete_dispute_caches(); } /** * Process notification data. * * @param array $event_body The event that triggered the webhook. * * @return void * * @throws Invalid_Webhook_Data_Exception When data is not valid. */ private function process_wcpay_notification( array $event_body ) { $note = $this->read_webhook_property( $event_body, 'data' ); // Convert exception Rest_Request_Exception to Invalid_Webhook_Data_Exception // to be compatible with the expected exception in process(). try { $this->remote_note_service->put_note( $note ); } catch ( Rest_Request_Exception $e ) { throw new Invalid_Webhook_Data_Exception( $e->getMessage() ); } } /** * Safely get a value from the webhook event body array. * * @param array $items Array to read from. * @param string $key ID to fetch on. * * @return string|array|int|bool * @throws Invalid_Webhook_Data_Exception Thrown if ID not set. */ private function read_webhook_property( $items, $key ) { if ( ! isset( $items[ $key ] ) ) { throw new Invalid_Webhook_Data_Exception( sprintf( /* translators: %1: ID being fetched */ __( '%1$s not found in array', 'woocommerce-payments' ), $key ) ); } return $items[ $key ]; } /** * Safely check whether a webhook contains a property. * * @param array $items Array to read from. * @param string $key ID to fetch on. * * @return bool */ private function has_webhook_property( $items, $key ) { return isset( $items[ $key ] ); } /** * Gets the order related to the event. * * @param array $event_body The event that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. * @throws Invalid_Payment_Method_Exception When unable to resolve intent ID to order. * * @return null|WC_Order */ private function get_order_from_event_body( $event_body ) { $event_data = $this->read_webhook_property( $event_body, 'data' ); $event_object = $this->read_webhook_property( $event_data, 'object' ); $intent_id = $this->read_webhook_property( $event_object, 'id' ); $order_key = $this->read_webhook_property( $event_object, 'metadata' )['order_key'] ?? null; // Look up the order related to this intent. $order = $this->wcpay_db->order_from_intent_id( $intent_id ); if ( ! $order instanceof \WC_Order ) { // Retrieving order with order_id in case intent_id was not properly set. Logger::debug( 'intent_id not found, using order_id to retrieve order' ); $metadata = $this->read_webhook_property( $event_object, 'metadata' ); $order_id = $metadata['order_id'] ?? null; // If metadata order id is null, try to read from the charges metadata. if ( null === $order_id ) { $charges = $this->read_webhook_property( $event_object, 'charges' ); $charge = $charges[0] ?? []; $order_id = $charge['metadata']['order_id'] ?? null; } if ( $order_id ) { $order = $this->wcpay_db->order_from_order_id( $order_id ); } elseif ( ! empty( $event_object['invoice'] ) ) { // If the payment intent contains an invoice it is a WCPay Subscription-related intent and will be handled by the `invoice.paid` event. return null; } } /** * If the order has been found, but there is an order key mismatch, it * could be caused by another site creating orders with the same IDs * while this site remains the primary webhook receiver. */ if ( null !== $order_key && $order instanceof WC_Order && $order->get_order_key() !== $order_key ) { Logger::debug( 'Mismatching order key found while retrieving an order for webhook processing', [ 'intent_id' => $intent_id, 'order_id' => $order->get_id(), 'webhook_order_key' => $order_key, 'local_order_key' => $order->get_order_key(), ] ); return null; } if ( ! $order instanceof \WC_Order ) { throw new Invalid_Payment_Method_Exception( sprintf( /* translators: %1: intent ID */ __( 'Could not find order via intent ID: %1$s', 'woocommerce-payments' ), $intent_id ), 'order_not_found' ); } return $order; } /** * Gets the proper failure message from the code in the error. * Error codes from https://stripe.com/docs/error-codes. * * @param array $error The last payment error from the payment failed event. * * @return string The failure message. */ private function get_failure_message_from_error( $error ): string { $code = $error['code'] ?? ''; $decline_code = $error['decline_code'] ?? ''; $message = $error['message'] ?? ''; switch ( $code ) { case 'account_closed': return __( "The customer's bank account has been closed.", 'woocommerce-payments' ); case 'debit_not_authorized': return __( 'The customer has notified their bank that this payment was unauthorized.', 'woocommerce-payments' ); case 'insufficient_funds': return __( "The customer's account has insufficient funds to cover this payment.", 'woocommerce-payments' ); case 'no_account': return __( "The customer's bank account could not be located.", 'woocommerce-payments' ); case 'payment_method_microdeposit_failed': return __( 'Microdeposit transfers failed. Please check the account, institution and transit numbers.', 'woocommerce-payments' ); case 'payment_method_microdeposit_verification_attempts_exceeded': return __( 'You have exceeded the number of allowed verification attempts.', 'woocommerce-payments' ); case 'payment_intent_mandate_invalid': return __( 'The mandate used for this renewal payment is invalid. You may need to bring the customer back to your store and ask them to resubmit their payment information.', 'woocommerce-payments' ); case 'card_declined': switch ( $decline_code ) { case 'debit_notification_undelivered': return __( "The customer's bank could not send pre-debit notification for the payment.", 'woocommerce-payments' ); case 'transaction_not_approved': return __( 'For recurring payment greater than mandate amount or INR 15000, payment was not approved by the card holder.', 'woocommerce-payments' ); } } // translators: %s Stripe error message. return sprintf( __( 'With the following message: <code>%s</code>', 'woocommerce-payments' ), $message ); } /** * Process webhook refund for events triggered externally. * * @param array $event_body The event that triggered the webhook. * * @throws Invalid_Webhook_Data_Exception Required parameters not found. * @throws Invalid_Webhook_Data_Exception When the refund amount is not valid. * @throws Order_Not_Found_Exception When unable to resolve charge ID to order. */ private function process_webhook_refund_triggered_externally( array $event_body ): void { $event_data = $this->read_webhook_property( $event_body, 'data' ); $event_object = $this->read_webhook_property( $event_data, 'object' ); $is_refunded_event = isset( $event_body['type'] ) && 'charge.refunded' === $event_body['type']; $status = $this->read_webhook_property( $event_object, 'status' ); if ( 'succeeded' !== $status || ! $is_refunded_event ) { return; } // Check if the charge was actually captured before processing the refund. // Stripe sends charge.refunded webhooks for cancelled authorizations even though no payment was captured. // We should not create WooCommerce refund objects for these cases as they cause negative values in analytics. $captured = $event_object['captured'] ?? false; if ( ! $captured ) { return; } // Fetch the details of the refund so that we can find the associated order and write a note. $charge_id = $this->read_webhook_property( $event_object, 'id' ); $refund = $this->read_webhook_property( $event_object, 'refunds' )['data'][0]; // Most recent refund. $refund_id = $refund['id'] ?? ''; $refund_reason = $refund['reason'] ?? ''; $refund_balance_transaction_id = $refund['balance_transaction'] ?? ''; $charge_amount = $this->read_webhook_property( $event_object, 'amount' ); $currency = $this->read_webhook_property( $event_object, 'currency' ); $refunded_amount = WC_Payments_Utils::interpret_stripe_amount( $refund['amount'], $currency ); $is_partial_refund = $refund['amount'] < $charge_amount; // Look up the order related to this charge. $order = $this->wcpay_db->order_from_charge_id( $charge_id ); if ( ! $order ) { throw new Order_Not_Found_Exception( sprintf( /* translators: %1: charge ID */ __( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ), $charge_id ), 'order_not_found' ); } // Only care about refunds that are triggered externally, i.e. outside WP Admin. // Refunds triggered in WP Admin are handled by WC_Payment_Gateway_WCPay::process_refund. $wc_refunds = $order->get_refunds(); if ( ! empty( $wc_refunds ) ) { foreach ( $wc_refunds as $wc_refund ) { $wcpay_refund_id = $this->order_service->get_wcpay_refund_id_for_order( $wc_refund ); if ( $refund_id === $wcpay_refund_id ) { return; } } } if ( $charge_amount < 0 || $refunded_amount > $order->get_total() ) { throw new Invalid_Webhook_Data_Exception( sprintf( /* translators: %1: charge ID */ __( 'The refund amount is not valid for charge ID: %1$s', 'woocommerce-payments' ), $charge_id ) ); } $wc_refund = $this->order_service->create_refund_for_order( $order, $refunded_amount, $refund_reason, ( ! $is_partial_refund ? $order->get_items() : [] ) ); // Process the refund in the order service. $this->order_service->add_note_and_metadata_for_created_refund( $order, $wc_refund, $refund_id, $refund_balance_transaction_id, Refund_Status::PENDING === $refund['status'] ); } /** * Process webhook for Stripe Billing invoice events. * * @param string $event_type The type of event that triggered the webhook. * @param array $event_body The event that triggered the webhook. * * @return void * * @throws Invalid_Webhook_Data_Exception When the linked subscription is not found. */ private function process_webhook_stripe_billing_invoice( $event_type, $event_body ) { if ( ! class_exists( 'WC_Payments_Subscriptions' ) ) { return; } switch ( $event_type ) { case 'invoice.upcoming': WC_Payments_Subscriptions::get_event_handler()->handle_invoice_upcoming( $event_body ); break; case 'invoice.paid': WC_Payments_Subscriptions::get_event_handler()->handle_invoice_paid( $event_body ); break; case 'invoice.payment_failed': WC_Payments_Subscriptions::get_event_handler()->handle_invoice_payment_failed( $event_body ); break; } } } includes/class-wc-payments-status.php 0000777 00000063351 15251156605 0014003 0 ustar 00 <?php /** * WC_Payments_Status class * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Hooks into Woo Status pages to provide extra tooling and information about WCPay. */ class WC_Payments_Status { /** * Instance of WC_Payment_Gateway_WCPay * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * Instance of WC_Payments_Http_Interface * * @var WC_Payments_Http_Interface */ private $http; /** * Instance of WC_Payments_Account * * @var WC_Payments_Account */ private $account; /** * WC_Payments_Status constructor. * * @param WC_Payment_Gateway_WCPay $gateway The main gateway instance. * @param WC_Payments_Http_Interface $http A class implementing WC_Payments_Http_Interface. * @param WC_Payments_Account $account The account service. */ public function __construct( $gateway, $http, $account ) { $this->gateway = $gateway; $this->http = $http; $this->account = $account; } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_action( 'woocommerce_system_status_report', [ $this, 'render_status_report_section' ], 1 ); add_filter( 'woocommerce_debug_tools', [ $this, 'debug_tools' ] ); } /** * Get the locations where an express checkout method is enabled. * * @param string $method_id The method identifier (payment_request, woopay, amazon_pay, link). * @return array List of locations where the method is enabled. */ private function get_express_checkout_method_locations( $method_id ) { $locations = [ 'product', 'cart', 'checkout' ]; $enabled_locations = []; foreach ( $locations as $location ) { $enabled_methods = $this->gateway->get_option( "express_checkout_{$location}_methods", [] ); if ( is_array( $enabled_methods ) && in_array( $method_id, $enabled_methods, true ) ) { $enabled_locations[] = $location; } } return $enabled_locations; } /** * Add WCPay tools to the Woo debug tools. * * @param array $tools List of current available tools. */ public function debug_tools( $tools ) { return array_merge( $tools, [ 'clear_wcpay_account_cache' => [ 'name' => sprintf( /* translators: %s: WooPayments */ __( 'Clear %s account cache', 'woocommerce-payments' ), 'WooPayments' ), 'button' => __( 'Clear', 'woocommerce-payments' ), 'desc' => sprintf( /* translators: %s: WooPayments */ __( 'This tool will clear the cached account values used in %s.', 'woocommerce-payments' ), 'WooPayments' ), 'callback' => [ $this->account, 'refresh_account_data' ], ], 'delete_wcpay_test_orders' => [ 'name' => sprintf( /* translators: %s: WooPayments */ __( 'Delete %s test orders', 'woocommerce-payments' ), 'WooPayments' ), 'button' => __( 'Delete', 'woocommerce-payments' ), 'desc' => sprintf( /* translators: %s: WooPayments */ __( '<strong class="red">Note:</strong> This option deletes all test mode orders placed via %s. Orders placed via other gateways will not be affected. Use with caution, as this action cannot be undone.', 'woocommerce-payments' ), 'WooPayments' ), 'callback' => [ $this, 'delete_test_orders' ], ], 'remediate_canceled_auth_fees_dry_run' => [ 'name' => __( 'Preview canceled authorization fix (Dry Run)', 'woocommerce-payments' ), 'button' => $this->get_dry_run_button_text(), 'desc' => __( 'Preview what orders would be affected by the canceled authorization fix without making any changes. Results are logged to WooCommerce > Status > Logs.', 'woocommerce-payments' ), 'callback' => [ $this, 'schedule_canceled_auth_dry_run' ], 'disabled' => $this->is_remediation_running_or_complete(), ], 'remediate_canceled_auth_fees' => [ 'name' => __( 'Fix canceled authorization analytics', 'woocommerce-payments' ), 'button' => $this->get_remediation_button_text(), 'desc' => $this->get_remediation_description(), 'confirm' => __( 'This will update order metadata and delete incorrect refund records for affected orders. This fixes negative values in WooCommerce Analytics. Make sure you have a recent backup before proceeding. Continue?', 'woocommerce-payments' ), 'callback' => [ $this, 'schedule_canceled_auth_remediation' ], 'disabled' => $this->is_remediation_running_or_complete(), ], ] ); } /** * Deletes all test orders created with WooPayments. * * Test orders are identified by the '_wcpay_mode' meta key with value 'test'. * * @return string Success or error message. */ public function delete_test_orders() { // Add explicit capability check. if ( ! current_user_can( 'manage_woocommerce' ) ) { return __( 'You do not have permission to delete orders.', 'woocommerce-payments' ); } try { // Get all orders with test mode meta. $test_orders = wc_get_orders( [ 'limit' => -1, // phpcs:ignore 'meta_key' => WC_Payments_Order_Service::WCPAY_MODE_META_KEY, // phpcs:ignore 'meta_value' => 'test', 'return' => 'objects', ] ); if ( empty( $test_orders ) ) { return __( 'No test orders found.', 'woocommerce-payments' ); } $deleted_count = 0; foreach ( $test_orders as $order ) { // Permanently delete the order (skip trash). if ( $order->delete( true ) ) { ++$deleted_count; } } return sprintf( /* translators: %d: number of orders deleted */ _n( '%d test order has been permanently deleted.', '%d test orders have been permanently deleted.', $deleted_count, 'woocommerce-payments' ), $deleted_count ); } catch ( Exception $e ) { return sprintf( /* translators: %s: error message */ __( 'Error deleting test orders: %s', 'woocommerce-payments' ), $e->getMessage() ); } } /** * Schedules the canceled authorization fee remediation. * * This tool fixes incorrect refund records and fee data from orders where * payment authorization was canceled but never captured. * * @return string Success or error message. */ public function schedule_canceled_auth_remediation() { // Add explicit capability check. if ( ! current_user_can( 'manage_woocommerce' ) ) { return __( 'You do not have permission to run this tool.', 'woocommerce-payments' ); } try { include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php'; $remediation = new WC_Payments_Remediate_Canceled_Auth_Fees(); // Check if already complete. if ( $remediation->is_complete() ) { return __( 'Remediation has already been completed.', 'woocommerce-payments' ); } // Check if already running (either full action or dry run). if ( function_exists( 'as_has_scheduled_action' ) ) { if ( as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK ) || as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::DRY_RUN_ACTION_HOOK ) ) { return __( 'Remediation is already in progress. Check the Action Scheduler for status.', 'woocommerce-payments' ); } } // Schedule the remediation. $remediation->schedule_remediation(); return __( 'Remediation has been scheduled and will run in the background. You can monitor progress in the Action Scheduler.', 'woocommerce-payments' ); } catch ( Exception $e ) { return sprintf( /* translators: %s: error message */ __( 'Error scheduling remediation: %s', 'woocommerce-payments' ), $e->getMessage() ); } } /** * Schedules the canceled authorization fee remediation dry run. * * This previews what orders would be affected without making changes. * * @return string Success or error message. */ public function schedule_canceled_auth_dry_run() { // Add explicit capability check. if ( ! current_user_can( 'manage_woocommerce' ) ) { return __( 'You do not have permission to run this tool.', 'woocommerce-payments' ); } try { include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php'; $remediation = new WC_Payments_Remediate_Canceled_Auth_Fees(); // Check if already complete. if ( $remediation->is_complete() ) { return __( 'Remediation has already been completed.', 'woocommerce-payments' ); } // Check if already running. if ( function_exists( 'as_has_scheduled_action' ) ) { if ( as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK ) || as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::DRY_RUN_ACTION_HOOK ) ) { return __( 'Remediation is already in progress. Check the Action Scheduler for status.', 'woocommerce-payments' ); } } // Schedule the dry run. $remediation->schedule_dry_run(); return __( 'Dry run has been scheduled and will run in the background. Check WooCommerce > Status > Logs for results (source: wcpay-fee-remediation).', 'woocommerce-payments' ); } catch ( Exception $e ) { return sprintf( /* translators: %s: error message */ __( 'Error scheduling dry run: %s', 'woocommerce-payments' ), $e->getMessage() ); } } /** * Get the button text for the dry run tool based on current status. * * @return string Button text. */ private function get_dry_run_button_text(): string { $status = get_option( 'wcpay_fee_remediation_status', '' ); if ( 'completed' === $status ) { return __( 'Completed', 'woocommerce-payments' ); } if ( 'running' === $status || $this->is_remediation_action_scheduled() ) { return __( 'Running...', 'woocommerce-payments' ); } return __( 'Preview', 'woocommerce-payments' ); } /** * Get the button text for the remediation tool based on current status. * * @return string Button text. */ private function get_remediation_button_text(): string { $status = get_option( 'wcpay_fee_remediation_status', '' ); if ( 'completed' === $status ) { return __( 'Completed', 'woocommerce-payments' ); } if ( 'running' === $status || $this->is_remediation_action_scheduled() ) { return __( 'Running...', 'woocommerce-payments' ); } return __( 'Run', 'woocommerce-payments' ); } /** * Get the description for the remediation tool including current status. * * @return string Tool description with status. */ private function get_remediation_description(): string { $base_desc = __( 'This tool removes incorrect refund records and fee data from orders where payment authorization was canceled (not captured). This fixes negative values appearing in WooCommerce Analytics for stores using manual capture.', 'woocommerce-payments' ); $status = get_option( 'wcpay_fee_remediation_status', '' ); if ( 'completed' === $status ) { $stats = get_option( 'wcpay_fee_remediation_stats', [] ); $processed = isset( $stats['processed'] ) ? (int) $stats['processed'] : 0; $remediated = isset( $stats['remediated'] ) ? (int) $stats['remediated'] : 0; if ( $processed > 0 ) { return sprintf( /* translators: 1: base description, 2: number of orders processed, 3: number of orders remediated */ __( '%1$s <strong>Status: Completed.</strong> Processed %2$d orders, remediated %3$d.', 'woocommerce-payments' ), $base_desc, $processed, $remediated ); } return sprintf( /* translators: %s: base description */ __( '%s <strong>Status: Completed.</strong> No affected orders found.', 'woocommerce-payments' ), $base_desc ); } if ( 'running' === $status || $this->is_remediation_action_scheduled() ) { $stats = get_option( 'wcpay_fee_remediation_stats', [] ); $processed = isset( $stats['processed'] ) ? (int) $stats['processed'] : 0; if ( $processed > 0 ) { return sprintf( /* translators: 1: base description, 2: number of orders processed so far */ __( '%1$s <strong>Status: Running...</strong> Processed %2$d orders so far. Check the Action Scheduler for details.', 'woocommerce-payments' ), $base_desc, $processed ); } return sprintf( /* translators: %s: base description */ __( '%s <strong>Status: Running...</strong> Check the Action Scheduler for details.', 'woocommerce-payments' ), $base_desc ); } return $base_desc; } /** * Check if the remediation is currently running or already complete. * * @return bool True if running or complete. */ private function is_remediation_running_or_complete(): bool { $status = get_option( 'wcpay_fee_remediation_status', '' ); if ( 'completed' === $status || 'running' === $status ) { return true; } return $this->is_remediation_action_scheduled(); } /** * Check if the remediation action is scheduled in Action Scheduler. * * @return bool True if action is scheduled. */ private function is_remediation_action_scheduled(): bool { if ( ! function_exists( 'as_has_scheduled_action' ) ) { return false; } include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php'; return as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK ); } /** * Renders WCPay information on the status page. */ public function render_status_report_section() { ?> <table class="wc_status_table widefat" cellspacing="0"> <thead> <tr> <th colspan="3" data-export-label="WooPayments"> <h2>WooPayments</h2> </th> </tr> </thead> <tbody> <tr> <td data-export-label="Version"><?php esc_html_e( 'Version', 'woocommerce-payments' ); ?>:</td> <td class="help"> <?php /* translators: %s: WooPayments */ echo wc_help_tip( sprintf( esc_html__( 'The current version of the %s extension.', 'woocommerce-payments' ), 'WooPayments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?> </td> <td><?php echo esc_html( WCPAY_VERSION_NUMBER ); ?></td> </tr> <tr> <td data-export-label="Connected to WPCOM"><?php esc_html_e( 'Connected to WPCOM', 'woocommerce-payments' ); ?>:</td> <td class="help"> <?php /* translators: %s: WooPayments */ echo wc_help_tip( sprintf( esc_html__( 'Can your store connect securely to wordpress.com? Without a proper WPCOM connection %s can\'t function!', 'woocommerce-payments' ), 'WooPayments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?> </td> <td><?php echo $this->http->is_connected() ? esc_html__( 'Yes', 'woocommerce-payments' ) : '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'No', 'woocommerce-payments' ) . '</mark>'; ?></td> </tr> <?php if ( $this->http->is_connected() ) : ?> <tr> <td data-export-label="WPCOM Blog ID"><?php esc_html_e( 'WPCOM Blog ID', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The corresponding wordpress.com blog ID for this store.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $this->http->is_connected() ? $this->http->get_blog_id() : '-' ); ?></td> </tr> <tr> <td data-export-label="Account ID"><?php esc_html_e( 'Account ID', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The merchant account ID you are currently using to process payments with.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo $this->gateway->is_connected() ? esc_html( $this->account->get_stripe_account_id() ?? '-' ) : '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Not connected', 'woocommerce-payments' ) . '</mark>'; ?></td> </tr> <?php // Only display the rest if the payment gateway is connected since many places check for this and we might get inaccurate data. if ( $this->gateway->is_connected() ) : ?> <tr> <td data-export-label="Payment Gateway"><?php esc_html_e( 'Payment Gateway', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Is the payment gateway ready and enabled for use on your store?', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo $this->gateway->needs_setup() ? '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Needs setup', 'woocommerce-payments' ) . '</mark>' : ( $this->gateway->is_enabled() ? esc_html__( 'Enabled', 'woocommerce-payments' ) : esc_html__( 'Disabled', 'woocommerce-payments' ) ); ?></td> </tr> <tr> <td data-export-label="Test Mode"><?php esc_html_e( 'Test Mode', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the payment gateway has test payments enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php WC_Payments::mode()->is_test() ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' ); ?></td> </tr> <tr> <td data-export-label="Enabled APMs"><?php esc_html_e( 'Enabled APMs', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'What payment methods are enabled for the store.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( implode( ',', $this->gateway->get_upe_enabled_payment_method_ids() ) ); ?></td> </tr> <?php if ( ! WC_Payments_Features::is_woopay_express_checkout_enabled() ) : ?> <tr> <td data-export-label="WooPay"><?php esc_html_e( 'WooPay Express Checkout', 'woocommerce-payments' ); ?>:</td> <td class="help"> <?php /* translators: %s: WooPayments */ echo wc_help_tip( sprintf( esc_html__( 'WooPay is not available, as a %s feature, or the store is not yet eligible.', 'woocommerce-payments' ), 'WooPayments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?> </td> <td><?php echo ! WC_Payments_Features::is_woopay_eligible() ? esc_html__( 'Not eligible', 'woocommerce-payments' ) : esc_html__( 'Not active', 'woocommerce-payments' ); ?></td> </tr> <?php else : ?> <tr> <td data-export-label="WooPay"><?php esc_html_e( 'WooPay Express Checkout', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the new WooPay Express Checkout is enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php $woopay_enabled_locations = $this->get_express_checkout_method_locations( 'woopay' ); $woopay_enabled_locations = empty( $woopay_enabled_locations ) ? 'no locations enabled' : implode( ',', $woopay_enabled_locations ); echo esc_html( WC_Payments_Features::is_woopay_enabled() ? __( 'Enabled', 'woocommerce-payments' ) . ' (' . $woopay_enabled_locations . ')' : __( 'Disabled', 'woocommerce-payments' ) ); ?> </td> </tr> <tr> <td data-export-label="WooPay Incompatible Extensions"><?php esc_html_e( 'WooPay Incompatible Extensions', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Whether there are extensions active that are have known incompatibilities with the functioning of the new WooPay Express Checkout.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php get_option( \WCPay\WooPay\WooPay_Scheduler::INVALID_EXTENSIONS_FOUND_OPTION_NAME, false ) ? esc_html_e( 'Yes', 'woocommerce-payments' ) : esc_html_e( 'No', 'woocommerce-payments' ); ?></td> </tr> <?php endif; ?> <tr> <td data-export-label="Apple Pay / Google Pay"><?php esc_html_e( 'Apple Pay / Google Pay Express Checkout', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the store has Payment Request enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php $payment_request_enabled = $this->gateway->is_payment_request_enabled(); $payment_request_enabled_locations = $this->get_express_checkout_method_locations( 'payment_request' ); $payment_request_enabled_locations = empty( $payment_request_enabled_locations ) ? 'no locations enabled' : implode( ',', $payment_request_enabled_locations ); echo esc_html( $payment_request_enabled ? __( 'Enabled', 'woocommerce-payments' ) . ' (' . $payment_request_enabled_locations . ')' : __( 'Disabled', 'woocommerce-payments' ) ); ?> </td> </tr> <tr> <td data-export-label="Fraud Protection Level"><?php esc_html_e( 'Fraud Protection Level', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The current fraud protection level the payment gateway is using.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php echo esc_html( $this->gateway->get_option( 'current_protection_level' ) ); ?></td> </tr> <?php if ( $this->gateway->get_option( 'current_protection_level' ) === 'advanced' ) : ?> <tr> <td data-export-label="Enabled Fraud Filters"><?php esc_html_e( 'Enabled Fraud Filters', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'The advanced fraud protection filters currently enabled.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php // Process the list. $adv_fraud_settings = json_decode( wp_json_encode( $this->gateway->get_option( 'advanced_fraud_protection_settings' ) ), true ); $list = array_filter( array_map( function ( $rule ) { if ( empty( $rule['key'] ) ) { return null; } switch ( $rule['key'] ) { case 'avs_verification': return 'AVS Verification'; case 'international_ip_address': return 'International IP Address'; case 'ip_address_mismatch': return 'IP Address Mismatch'; case 'address_mismatch': return 'Address Mismatch'; case 'purchase_price_threshold': return 'Purchase Price Threshold'; case 'order_items_threshold': return 'Order Items Threshold'; default: // Ignore all others. return null; } }, $adv_fraud_settings ) ); echo empty( $list ) ? '-' : esc_html( implode( ',', $list ) ); ?> </td> </tr> <?php endif; ?> <tr> <td data-export-label="Multi-currency"><?php esc_html_e( 'Multi-currency', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the store has the Multi-currency feature enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php WC_Payments_Features::is_customer_multi_currency_enabled() ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' ); ?></td> </tr> <tr> <td data-export-label="Auth and Capture"><?php esc_html_e( 'Auth and Capture', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the store has the Auth & Capture feature enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td> <?php $manual_capture_enabled = 'yes' === $this->gateway->get_option( 'manual_capture' ); echo $manual_capture_enabled ? esc_html__( 'Enabled', 'woocommerce-payments' ) : esc_html__( 'Disabled', 'woocommerce-payments' ); ?> </td> </tr> <tr> <td data-export-label="Documents"><?php esc_html_e( 'Documents', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Whether the tax documents section is enabled or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php WC_Payments_Features::is_documents_section_enabled() ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' ); ?></td> </tr> <?php endif; // Gateway connected. ?> <?php endif; // Connected to WPCOM. ?> <tr> <td data-export-label="Logging"><?php esc_html_e( 'Logging', 'woocommerce-payments' ); ?>:</td> <td class="help"><?php echo wc_help_tip( esc_html__( 'Whether debug logging is enabled and working or not.', 'woocommerce-payments' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.OutputNotEscaped */ ?></td> <td><?php \WCPay\Logger::can_log() ? esc_html_e( 'Enabled', 'woocommerce-payments' ) : esc_html_e( 'Disabled', 'woocommerce-payments' ); ?></td> </tr> </tbody> </table> <?php } } includes/fraud-prevention/models/class-check.php 0000777 00000013261 15251156605 0016036 0 ustar 00 <?php /** * The check model class. * * @package WCPay\Fraud_Prevention\Models */ namespace WCPay\Fraud_Prevention\Models; use WCPay\Exceptions\Fraud_Ruleset_Exception; /** * Check model. */ class Check { // Check operators. const OPERATOR_EQUALS = 'equals'; const OPERATOR_NOT_EQUALS = 'not_equals'; const OPERATOR_GTE = 'greater_or_equal'; const OPERATOR_GT = 'greater_than'; const OPERATOR_LTE = 'less_or_equal'; const OPERATOR_LT = 'less_than'; const OPERATOR_IN = 'in'; const OPERATOR_NOT_IN = 'not_in'; // Checklist operators. const LIST_OPERATOR_AND = 'and'; const LIST_OPERATOR_OR = 'or'; /** * List of check operators. * * @var array */ private static $check_operators = [ self::OPERATOR_EQUALS, self::OPERATOR_NOT_EQUALS, self::OPERATOR_GT, self::OPERATOR_GTE, self::OPERATOR_LT, self::OPERATOR_LTE, self::OPERATOR_IN, self::OPERATOR_NOT_IN, ]; /** * List of checklist operators. * * @var array */ private static $list_operators = [ self::LIST_OPERATOR_AND, self::LIST_OPERATOR_OR, ]; /** * Operator for the Check. * * @var string */ public $operator = null; /** * The key of the source which contains the data. Is mapped to a real data to compare with the value on the Fraud_Ruleset_Service. * * @var string */ public $key = null; /** * Value to check against the source. * * @var mixed */ public $value = null; /** * Subchecks array that when filled, indicates this is a checklist. * * @var array */ public $checks = []; /** * Creates a Check instance from an array. * * @param array $array The Check configuration. * * @return Check * @throws Fraud_Ruleset_Exception When the array validation fails. */ public static function from_array( array $array ): Check { // Check if this is a valid candidate for a rule. Rules should have keys, outcomes, and checks defined and not empty. if ( ! self::validate_array( $array ) ) { throw new Fraud_Ruleset_Exception( 'Check definition not valid.' ); } $check = new self(); $check->key = $array['key'] ?? null; $check->operator = $array['operator']; $check->value = $array['value'] ?? null; if ( isset( $array['checks'] ) ) { foreach ( $array['checks'] as $check_definition ) { $check->checks[] = self::from_array( $check_definition ); } } return $check; } /** * Validates the given array if it's structured to become a Check object. * * @param array $array The array to validate. * * @return bool Whether it is a valid Check array. */ public static function validate_array( array $array ): bool { // Check if this array contains an operator. In all cases it should have an operator field. if ( ! isset( $array['operator'] ) ) { return false; } if ( in_array( $array['operator'], self::$list_operators, true ) ) { // This should be a checklist, and should have checks. if ( ! isset( $array['checks'] ) || empty( $array['checks'] ) ) { return false; } // Validate child checks. foreach ( $array['checks'] as $check ) { if ( ! self::validate_array( $check ) ) { return false; } } } elseif ( in_array( $array['operator'], self::$check_operators, true ) ) { // This should be a single check, and should have key and value. if ( ! isset( $array['value'] ) ) { return false; } if ( ! isset( $array['key'] ) ) { return false; } } else { return false; } return true; } /** * Creates a list type of check with the given parameters. * * @param string $operator The checklist operator. * @param array $checks The child checks array. * * @return Check * @throws Fraud_Ruleset_Exception When the validation fails. */ public static function list( string $operator, array $checks ) { if ( ! in_array( $operator, self::$list_operators, true ) ) { // $operator is a predefined constant, no need to escape. // phpcs:ignore WordPress.Security.EscapeOutput throw new Fraud_Ruleset_Exception( 'Operator for the check is invalid: ' . $operator ); } if ( 0 < count( array_filter( $checks, function ( $check ) { return ! ( $check instanceof Check ); } ) ) ) { throw new Fraud_Ruleset_Exception( 'The checklist checks should only contain Check objects.' ); } $checklist = new Check(); $checklist->operator = $operator; $checklist->checks = $checks; return $checklist; } /** * Creates a list type of check with the given parameters. * * @param string $key The key of the check. * @param string $operator The check operator. * @param mixed $value The value to compare against. * * @return Check * @throws Fraud_Ruleset_Exception When the validation fails. */ public static function check( string $key, string $operator, $value ) { if ( ! in_array( $operator, self::$check_operators, true ) ) { // $operator is a predefined constant, no need to escape. // phpcs:ignore WordPress.Security.EscapeOutput throw new Fraud_Ruleset_Exception( 'Operator for the check is invalid: ' . $operator ); } $check = new Check(); $check->operator = $operator; $check->key = $key; $check->value = $value; return $check; } /** * Converts the class to it's array representation for transmission. * * @return array */ public function to_array() { if ( ! empty( $this->checks ) ) { return [ 'operator' => $this->operator, 'checks' => array_map( function ( Check $check ) { return $check->to_array(); }, $this->checks ), ]; } return [ 'key' => $this->key, 'operator' => $this->operator, 'value' => $this->value, ]; } } includes/fraud-prevention/models/class-rule.php 0000777 00000006466 15251156605 0015741 0 ustar 00 <?php /** * The rule model class. * * @package WCPay\Fraud_Prevention\Models */ namespace WCPay\Fraud_Prevention\Models; use WCPay\Exceptions\Fraud_Ruleset_Exception; /** * Rule model. */ class Rule { /** * Constants that define the outcome of the rule. * * @var string */ const FRAUD_OUTCOME_ALLOW = 'allow'; const FRAUD_OUTCOME_REVIEW = 'review'; const FRAUD_OUTCOME_BLOCK = 'block'; /** * Rule key. * * @var string */ public $key; /** * The action to take when the rule is successful. * * @var string */ public $outcome; /** * The check or checklist that defines the rule clause. * * @var Check */ public $check; /** * Class constructor. * * @param string $key The rule key. * @param string $outcome The rule outcome. * @param Check $check The single check, or the wrapper checklist. * * @return void * @throws Fraud_Ruleset_Exception When the outcome validation fails. */ public function __construct( string $key, string $outcome, Check $check ) { if ( ! in_array( $outcome, [ self::FRAUD_OUTCOME_ALLOW, self::FRAUD_OUTCOME_BLOCK, self::FRAUD_OUTCOME_REVIEW ], true ) ) { throw new Fraud_Ruleset_Exception( 'Given rule outcome is invalid.' ); } $this->key = $key; $this->outcome = $outcome; $this->check = $check; } /** * Creates a Rule instance from a Fraud_Ruleset rule_config field. * * @param array $array The rule array retrieved from parsing Fraud_Ruleset::rules_config. * * @return Rule * @throws Fraud_Ruleset_Exception */ public static function from_array( array $array ): Rule { // Check if this is a valid candidate for a rule. Rules should have keys, outcomes, and checks defined and not empty. if ( ! self::validate_array( $array ) ) { throw new Fraud_Ruleset_Exception( 'Rule definition not valid.' ); } return new self( $array['key'], $array['outcome'], Check::from_array( $array['check'] ) ); } /** * Validates the given array if it's structured to become a Rule object. * * @param array $array The array to validate. * * @return bool Whether it is a valid Rule array. */ public static function validate_array( array $array ) { if ( ! isset( $array['key'], $array['check'], $array['outcome'] ) || ! is_array( $array['check'] ) || empty( $array['check'] ) || ! in_array( $array['outcome'], [ self::FRAUD_OUTCOME_BLOCK, self::FRAUD_OUTCOME_REVIEW, self::FRAUD_OUTCOME_ALLOW, ], true ) ) { return false; } // Validate child checks. if ( ! Check::validate_array( $array['check'] ) ) { return false; } return true; } /** * Validates the given string to see if it's a valid fraud outcome status. * * @param string $outcome The array to validate. * * @return bool Whether it is a valid Rule array. */ public static function is_valid_fraud_outcome_status( string $outcome ): bool { return in_array( $outcome, [ self::FRAUD_OUTCOME_BLOCK, self::FRAUD_OUTCOME_REVIEW, self::FRAUD_OUTCOME_ALLOW, ], true ); } /** * Converts the class to it's array representation for transmission. * * @return array */ public function to_array() { return [ 'key' => $this->key, 'outcome' => $this->outcome, 'check' => $this->check->to_array(), ]; } } includes/fraud-prevention/class-buyer-fingerprinting-service.php 0000777 00000005042 15251156605 0021303 0 ustar 00 <?php /** * Class Buyer_Fingerprinting_Service * * @package WCPay\Fraud_Prevention */ namespace WCPay\Fraud_Prevention; use WC_Geolocation; /** * Class Buyer_Fingerprinting_Service */ class Buyer_Fingerprinting_Service { /** * Singleton instance. * * @var Buyer_Fingerprinting_Service */ private static $instance; /** * Returns singleton instance. * * @return Buyer_Fingerprinting_Service */ public static function get_instance(): self { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Sets a instance to be used in request cycle. * Introduced primarily for supporting unit tests. * * @param Buyer_Fingerprinting_Service|null $instance Instance of self. */ public static function set_instance( ?self $instance = null ) { self::$instance = $instance; } /** * Hashes customer data for the fraud prevention. * * @param string $data The data you want to hash. * * @return string Hashed data. */ public function hash_data_for_fraud_prevention( string $data ): string { return hash( 'sha512', $data, false ); } /** * Returns fraud prevention data for an order. * * @param string $fingerprint User fingerprint. * * @return array An array of hashed data for an order. */ public function get_hashed_data_for_customer( $fingerprint ): array { global $wp; $order_items_count = WC()->cart ? intval( WC()->cart->get_cart_contents_count() ) : null; $order_id = null; if ( isset( $wp->query_vars['order-pay'] ) ) { $order_id = absint( $wp->query_vars['order-pay'] ); } elseif ( isset( $_POST['wcpay_order_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $order_id = absint( $_POST['wcpay_order_id'] ); // phpcs:ignore WordPress.Security.NonceVerification } if ( ! $order_items_count && 0 < $order_id ) { $order = wc_get_order( $order_id ); if ( $order ) { $order_items_count = $order->get_item_count(); } } // According to https://www.php.net/manual/en/function.array-filter.php#111091 // Applying "strlen" as the callback function will remove `false`, `null` and empty strings, but not "0" values. return array_filter( [ 'fraud_prevention_data_shopper_ip_hash' => $this->hash_data_for_fraud_prevention( WC_Geolocation::get_ip_address() ), 'fraud_prevention_data_shopper_ua_hash' => $fingerprint, 'fraud_prevention_data_ip_country' => WC_Geolocation::geolocate_ip( '', true )['country'], 'fraud_prevention_data_cart_contents' => $order_items_count, ], 'strlen' ); } } includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php 0000777 00000031743 15251156605 0021135 0 ustar 00 <?php /** * Class Order_Fraud_And_Risk_Meta_Box * * @package WCPay\Fraud_Prevention */ namespace WCPay\Fraud_Prevention; use WC_Payments_Admin_Settings; use WC_Payments_Order_Service; use WC_Payments_Utils; use WCPay\Constants\Fraud_Meta_Box_Type; use WCPay\Fraud_Prevention\Models\Rule; /** * Class Order_Fraud_And_Risk_Meta_Box */ class Order_Fraud_And_Risk_Meta_Box { /** * The Order Service. * * @var WC_Payments_Order_Service */ private $order_service; /** * Constructor. * * @param WC_Payments_Order_Service $order_service The order service. */ public function __construct( WC_Payments_Order_Service $order_service ) { $this->order_service = $order_service; } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_action( 'add_meta_boxes', [ $this, 'maybe_add_meta_box' ] ); } /** * Maybe add the meta box. */ public function maybe_add_meta_box() { // If we cannot get the screen ID, exit. if ( ! function_exists( '\wc_get_page_screen_id' ) ) { return; } // Get the order edit screen to be able to add the meta box to. $wc_screen_id = \wc_get_page_screen_id( 'shop-order' ); add_meta_box( 'wcpay-order-fraud-and-risk-meta-box', __( 'Fraud & Risk', 'woocommerce-payments' ), [ $this, 'display_order_fraud_and_risk_meta_box_message' ], $wc_screen_id, 'side', 'default' ); } /** * Displays the contents of the Fraud & Risk meta box. * * @param \WC_Order $order The order we are working with. * * @return void */ public function display_order_fraud_and_risk_meta_box_message( $order ) { $order = wc_get_order( $order ); if ( ! $order ) { return; } $intent_id = $this->order_service->get_intent_id_for_order( $order ); $charge_id = $this->order_service->get_charge_id_for_order( $order ); $meta_box_type = $this->order_service->get_fraud_meta_box_type_for_order( $order ); $risk_level = $this->order_service->get_charge_risk_level_for_order( $order ); $payment_method = $order->get_payment_method(); if ( strstr( $payment_method, 'woocommerce_payments_' ) ) { $meta_box_type = Fraud_Meta_Box_Type::NOT_CARD; } elseif ( 'woocommerce_payments' !== $payment_method ) { $meta_box_type = Fraud_Meta_Box_Type::NOT_WCPAY; } $icons = [ 'green_check_mark' => [ 'url' => plugins_url( 'assets/images/icons/check-green.svg', WCPAY_PLUGIN_FILE ), 'alt' => __( 'Green check mark', 'woocommerce-payments' ), ], 'orange_shield' => [ 'url' => plugins_url( 'assets/images/icons/shield-stroke-orange.svg', WCPAY_PLUGIN_FILE ), 'alt' => __( 'Orange shield outline', 'woocommerce-payments' ), ], 'red_shield' => [ 'url' => plugins_url( 'assets/images/icons/shield-stroke-red.svg', WCPAY_PLUGIN_FILE ), 'alt' => __( 'Red shield outline', 'woocommerce-payments' ), ], ]; $statuses = [ 'blocked' => __( 'Blocked', 'woocommerce-payments' ), 'approved' => __( 'Approved', 'woocommerce-payments' ), 'held_for_review' => __( 'Held for review', 'woocommerce-payments' ), 'no_action_taken' => __( 'No action taken', 'woocommerce-payments' ), ]; $risk_filters_callout = __( 'Adjust risk filters', 'woocommerce-payments' ); $risk_filters_url = WC_Payments_Admin_Settings::get_settings_url( [ 'anchor' => '%23fp-settings' ] ); $show_adjust_risk_filters_link = true; $this->maybe_print_risk_level_block( $risk_level ); echo '<div class="wcpay-fraud-risk-action">'; switch ( $meta_box_type ) { case Fraud_Meta_Box_Type::ALLOW: $description = __( 'The payment for this order passed your risk filtering.', 'woocommerce-payments' ); echo '<p class="wcpay-fraud-risk-meta-allow"><img src="' . esc_url( $icons['green_check_mark']['url'] ) . '" alt="' . esc_html( $icons['green_check_mark']['alt'] ) . '"> ' . esc_html( $statuses['no_action_taken'] ) . '</p><p>' . esc_html( $description ) . '</p>'; break; case Fraud_Meta_Box_Type::BLOCK: $description = __( 'The payment for this order was blocked by your risk filtering. There is no pending authorization, and the order can be cancelled to reduce any held stock.', 'woocommerce-payments' ); $callout = __( 'View more details', 'woocommerce-payments' ); $transaction_url = $this->compose_transaction_url_with_tracking( $order->get_id(), '', Rule::FRAUD_OUTCOME_BLOCK ); echo '<p class="wcpay-fraud-risk-meta-blocked"><img src="' . esc_url( $icons['red_shield']['url'] ) . '" alt="' . esc_html( $icons['red_shield']['alt'] ) . '"> ' . esc_html( $statuses['blocked'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>'; break; case Fraud_Meta_Box_Type::NOT_CARD: case Fraud_Meta_Box_Type::NOT_WCPAY: $payment_method_title = $order->get_payment_method_title(); $show_adjust_risk_filters_link = false; if ( ! empty( $payment_method_title ) && 'Popular payment methods' !== $payment_method_title ) { $description = sprintf( /* translators: %1: WooPayments, %2: Payment method title */ __( 'Risk filtering is only available for orders processed using credit cards with %1$s. This order was processed with %2$s.', 'woocommerce-payments' ), 'WooPayments', $payment_method_title ); } else { $description = sprintf( /* translators: %s: WooPayments */ __( 'Risk filtering is only available for orders processed using credit cards with %s.', 'woocommerce-payments' ), 'WooPayments' ); } $callout = __( 'Learn more', 'woocommerce-payments' ); $callout_url = 'https://woocommerce.com/document/woopayments/fraud-and-disputes/fraud-protection/'; $callout_url = add_query_arg( 'status_is', 'fraud-meta-box-not-wcpay-learn-more', $callout_url ); echo '<p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $callout_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>'; break; case Fraud_Meta_Box_Type::PAYMENT_STARTED: $description = __( 'The payment for this order has not yet been passed to the fraud and risk filters to determine its outcome status.', 'woocommerce-payments' ); echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['no_action_taken'] ) . '</p><p>' . esc_html( $description ) . '</p>'; break; case Fraud_Meta_Box_Type::REVIEW: $description = __( 'The payment for this order was held for review by your risk filtering. You can review the details and determine whether to approve or block the payment.', 'woocommerce-payments' ); $callout = __( 'Review payment', 'woocommerce-payments' ); $transaction_url = $this->compose_transaction_url_with_tracking( $intent_id, $charge_id, Rule::FRAUD_OUTCOME_REVIEW ); echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>'; break; case Fraud_Meta_Box_Type::REVIEW_ALLOWED: $description = __( 'The payment for this order was held for review by your risk filtering and manually approved.', 'woocommerce-payments' ); echo '<p class="wcpay-fraud-risk-meta-allow"><img src="' . esc_url( $icons['green_check_mark']['url'] ) . '" alt="' . esc_html( $icons['green_check_mark']['alt'] ) . '"> ' . esc_html( $statuses['approved'] ) . '</p><p>' . esc_html( $description ) . '</p>'; break; case Fraud_Meta_Box_Type::REVIEW_BLOCKED: $description = __( 'This transaction was held for review by your risk filters, and the charge was manually blocked after review.', 'woocommerce-payments' ); $callout = __( 'Review payment', 'woocommerce-payments' ); $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); echo '<p class="wcpay-fraud-risk-meta-blocked"><img src="' . esc_url( $icons['red_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>'; break; case Fraud_Meta_Box_Type::REVIEW_EXPIRED: $description = __( 'The payment for this order was held for review by your risk filtering. The authorization for the charge appears to have expired.', 'woocommerce-payments' ); $callout = __( 'Review payment', 'woocommerce-payments' ); $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>'; break; case Fraud_Meta_Box_Type::REVIEW_FAILED: $description = __( 'The payment for this order was held for review by your risk filtering. The authorization for the charge appears to have failed.', 'woocommerce-payments' ); $callout = __( 'Review payment', 'woocommerce-payments' ); $transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>'; break; case Fraud_Meta_Box_Type::TERMINAL_PAYMENT: $description = __( 'The payment for this order was done in person and has bypassed your risk filtering.', 'woocommerce-payments' ); echo '<p class="wcpay-fraud-risk-meta-allow"><img src="' . esc_url( $icons['green_check_mark']['url'] ) . '" alt="' . esc_html( $icons['green_check_mark']['alt'] ) . '"> ' . esc_html( $statuses['no_action_taken'] ) . '</p><p>' . esc_html( $description ) . '</p>'; break; default: $description = sprintf( /* translators: %s: WooPayments */ __( 'Risk filtering through %s was not found on this order, it may have been created while filtering was not enabled.', 'woocommerce-payments' ), 'WooPayments' ); echo '<p>' . esc_html( $description ) . '</p>'; break; } if ( $show_adjust_risk_filters_link ) { echo '<p><a href="' . esc_url( $risk_filters_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $risk_filters_callout ) . '</a></p>'; } echo '</div>'; } /** * Prints the risk level block. * * @param string $risk_level The risk level to display. * * @return void */ private function maybe_print_risk_level_block( $risk_level ) { $valid_risk_levels = [ 'normal', 'elevated', 'highest' ]; if ( ! in_array( $risk_level, $valid_risk_levels, true ) ) { return; } $titles = [ 'normal' => __( 'Normal', 'woocommerce-payments' ), 'elevated' => __( 'Elevated', 'woocommerce-payments' ), 'highest' => __( 'High', 'woocommerce-payments' ), ]; $descriptions = [ 'normal' => __( 'This payment shows a lower than normal risk of fraudulent activity.', 'woocommerce-payments' ), 'elevated' => __( 'This order has a moderate risk of being fraudulent. We suggest contacting the customer to confirm their details before fulfilling it.', 'woocommerce-payments' ), 'highest' => __( 'This order has a high risk of being fraudulent. We suggest contacting the customer to confirm their details before fulfilling it.', 'woocommerce-payments' ), ]; echo '<div class="wcpay-fraud-risk-level wcpay-fraud-risk-level--' . esc_attr( $risk_level ) . '">'; echo '<p class="wcpay-fraud-risk-level__title">' . esc_html( $titles[ $risk_level ] ) . '</p>'; echo '<div class="wcpay-fraud-risk-level__bar"></div>'; echo '<p>' . esc_html( $descriptions[ $risk_level ] ) . '</p>'; echo '</div>'; } /** * Composes url for transaction details page. * * @param string $primary_id Usually the Payment Intent ID, but can be an order ID. * @param string $fallback_id Usually the Charge ID. * @param string $status The status we're wanting to add to the meta box tracking. * * @return string Transaction details page url with tracking. */ private function compose_transaction_url_with_tracking( $primary_id, $fallback_id, $status ) { return WC_Payments_Utils::compose_transaction_url( $primary_id, $fallback_id, [ 'status_is' => $status, 'type_is' => 'meta_box', ] ); } } includes/fraud-prevention/class-fraud-risk-tools.php 0000777 00000026054 15251156605 0016707 0 ustar 00 <?php /** * Class Fraud_Risk_Tools * * @package WooCommerce\Payments\Fraud_Risk_Tools */ namespace WCPay\Fraud_Prevention; require_once __DIR__ . '/models/class-check.php'; require_once __DIR__ . '/models/class-rule.php'; use WC_Payments; use WC_Payments_Account; use WC_Payments_Features; use WCPay\Fraud_Prevention\Models\Check; use WCPay\Fraud_Prevention\Models\Rule; use WCPay\Constants\Currency_Code; defined( 'ABSPATH' ) || exit; /** * Class that controls Fraud and Risk tools functionality. */ class Fraud_Risk_Tools { /** * The single instance of the class. * * @var ?Fraud_Risk_Tools */ protected static $instance = null; /** * Instance of WC_Payments_Account. * * @var WC_Payments_Account */ private $payments_account; /** * Main Fraud_Risk_Tools Instance. * * Ensures only one instance of Fraud_Risk_Tools is loaded or can be loaded. * * @static * @return Fraud_Risk_Tools - Main instance. */ public static function instance() { if ( is_null( self::$instance ) ) { self::$instance = new self( WC_Payments::get_account_service() ); self::$instance->init_hooks(); } return self::$instance; } // Rule names. const RULE_ADDRESS_MISMATCH = 'address_mismatch'; const RULE_INTERNATIONAL_IP_ADDRESS = 'international_ip_address'; const RULE_IP_ADDRESS_MISMATCH = 'ip_address_mismatch'; const RULE_ORDER_ITEMS_THRESHOLD = 'order_items_threshold'; const RULE_PURCHASE_PRICE_THRESHOLD = 'purchase_price_threshold'; /** * Class constructor. * * @param WC_Payments_Account $payments_account WC_Payments_Account instance. */ public function __construct( WC_Payments_Account $payments_account ) { $this->payments_account = $payments_account; } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) { add_action( 'admin_menu', [ $this, 'init_advanced_settings_page' ] ); } } /** * Initialize the Fraud & Risk Tools Advanced Settings Page. * * @return void */ public function init_advanced_settings_page() { // Settings page generation on the incoming CLI and async job calls. if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'WPCOM_JOBS' ) && WPCOM_JOBS ) ) { return; } if ( ! $this->payments_account->is_stripe_account_valid() ) { return; } if ( ! function_exists( 'wc_admin_register_page' ) ) { return; } wc_admin_register_page( [ 'id' => 'wc-payments-fraud-protection', 'title' => __( 'Fraud protection', 'woocommerce-payments' ), 'parent' => 'wc-payments', 'path' => '/payments/fraud-protection', 'nav_args' => [ 'parent' => 'wc-payments', 'order' => 50, ], ] ); remove_submenu_page( 'wc-admin&path=/payments/overview', 'wc-admin&path=/payments/fraud-protection' ); } /** * Returns the basic protection rules. * * @return array */ public static function get_basic_protection_settings() { $rules = []; return self::get_ruleset_array( $rules ); } /** * Validates the array to see if it's a valid ruleset. * * @param array $array The array to validate. * * @return bool Whether if the given array is a ruleset, or not. */ public static function is_valid_ruleset_array( array $array ) { foreach ( $array as $rule ) { if ( ! Rule::validate_array( $rule ) ) { return false; } } return true; } /** * Returns the international IP address rule. * * @return Rule International IP address rule object. */ public static function get_international_ip_address_rule() { return new Rule( self::RULE_INTERNATIONAL_IP_ADDRESS, WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'ip_country', self::get_selling_locations_type_operator(), self::get_selling_locations_string() ) ); } /** * Returns the standard protection rules. * * @return array */ public static function get_standard_protection_settings() { $rules = [ // REVIEW An order originates from an IP address outside your country. self::get_international_ip_address_rule(), // REVIEW An order exceeds $1,000.00 or 10 items. new Rule( self::RULE_ORDER_ITEMS_THRESHOLD, WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'item_count', Check::OPERATOR_GT, 10 ) ), // REVIEW An order exceeds $1,000.00 or 10 items. new Rule( self::RULE_PURCHASE_PRICE_THRESHOLD, WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'order_total', Check::OPERATOR_GT, self::get_formatted_converted_amount( 1000 * 100, strtolower( Currency_Code::UNITED_STATES_DOLLAR ) ) ) ), // REVIEW An order is originated from a different country than the shipping country. new Rule( self::RULE_IP_ADDRESS_MISMATCH, WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'ip_billing_country_same', Check::OPERATOR_EQUALS, false ) ), ]; return self::get_ruleset_array( $rules ); } /** * Returns the default protection settings. * * @return array */ public static function get_high_protection_settings() { $rules = [ // BLOCK An order originates from an IP address outside your country. new Rule( self::RULE_INTERNATIONAL_IP_ADDRESS, Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'ip_country', self::get_selling_locations_type_operator(), self::get_selling_locations_string() ) ), // BLOCK An order exceeds $1,000.00. new Rule( self::RULE_PURCHASE_PRICE_THRESHOLD, Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'order_total', Check::OPERATOR_GT, self::get_formatted_converted_amount( 1000 * 100, strtolower( Currency_Code::UNITED_STATES_DOLLAR ) ) ) ), // REVIEW An order has less than 2 items or more than 10 items. new Rule( self::RULE_ORDER_ITEMS_THRESHOLD, WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::list( Check::LIST_OPERATOR_OR, [ Check::check( 'item_count', Check::OPERATOR_LT, 2 ), Check::check( 'item_count', Check::OPERATOR_GT, 10 ), ] ) ), // REVIEW The shipping and billing address don't match. new Rule( self::RULE_ADDRESS_MISMATCH, WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'billing_shipping_address_same', Check::OPERATOR_EQUALS, false ) ), // REVIEW An order is originated from a different country than the shipping country. new Rule( self::RULE_IP_ADDRESS_MISMATCH, WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK, Check::check( 'ip_billing_country_same', Check::OPERATOR_EQUALS, false ) ), ]; return self::get_ruleset_array( $rules ); } /** * Returns the matching predef for a given ruleset array, if nothing matches, returns "advanced". * * @param array $fraud_ruleset The ruleset config to match to. * * @return string The matching protection level. */ public static function get_matching_protection_level( $fraud_ruleset ) { // Check if the ruleset contains the basic protection config. $target_ruleset = self::get_basic_protection_settings(); if ( $target_ruleset === $fraud_ruleset ) { return 'basic'; } // Check if the ruleset contains the standard protection config. $target_ruleset = self::get_standard_protection_settings(); if ( $target_ruleset === $fraud_ruleset ) { return 'standard'; } // Check if the ruleset contains the high protection config. $target_ruleset = self::get_high_protection_settings(); if ( $target_ruleset === $fraud_ruleset ) { return 'high'; } // The ruleset contains custom configuration. return 'advanced'; } /** * Returns the array representation of ruleset. * * @param array $array The array of Rule objects. * * @return array */ private static function get_ruleset_array( $array ) { return array_map( function ( Rule $rule ) { return $rule->to_array(); }, $array ); } /** * Returns the check operator for international checks according to the WC Core selling locations setting. * * @return string The related operator. */ private static function get_selling_locations_type_operator() { $selling_locations_type = get_option( 'woocommerce_allowed_countries', 'all' ); if ( 'specific' === $selling_locations_type ) { return Check::OPERATOR_NOT_IN; } return Check::OPERATOR_IN; } /** * Returns the countries to sell to, or not, as a | delimited string array. * * @return string The array imploded with | character. */ private static function get_selling_locations_string() { $selling_locations_type = get_option( 'woocommerce_allowed_countries', 'all' ); switch ( $selling_locations_type ) { case 'specific': return strtolower( implode( '|', get_option( 'woocommerce_specific_allowed_countries', [] ) ) ); case 'all_except': return strtolower( implode( '|', get_option( 'woocommerce_all_except_countries', [] ) ) ); case 'all': return ''; default: return ''; } } /** * Returns the converted amount from a given currency to the default currency. * * @param int $amount The amount to be converted. * @param string $from The currency to be converted from. * @param string $to The currency to be converted to. * * @return int */ private static function get_converted_amount( $amount, $from, $to ) { $to_currency = strtoupper( $to ); $from_currency = strtoupper( $from ); $enabled_currencies = WC_Payments_Multi_Currency()->get_enabled_currencies(); if ( empty( $enabled_currencies ) || $to_currency === $from_currency ) { return $amount; } if ( array_key_exists( $from_currency, $enabled_currencies ) ) { $currency = $enabled_currencies[ $from_currency ]; $amount = (int) round( $amount * ( 1 / (float) $currency->get_rate() ) ); } return $amount; } /** * Returns the formatted converted amount from a given currency to the default currency. * The final format is "AMOUNT|CURRENCY". * * @param int $amount The amount to be converted. * @param string $base_currency The currency to be converted from. * * @return string */ private static function get_formatted_converted_amount( $amount, $base_currency ) { $default_currency = $base_currency; $target_currency = $base_currency; if ( function_exists( 'WC_Payments_Multi_Currency' ) ) { $default_currency = WC_Payments_Multi_Currency()->get_default_currency(); if ( ! empty( $default_currency ) ) { $target_currency = $default_currency->get_code(); $amount = self::get_converted_amount( $amount, $base_currency, $target_currency ); } } return implode( '|', [ $amount, strtolower( $target_currency ) ] ); } } includes/fraud-prevention/wc-payments-fraud-risk-tools.php 0000777 00000000773 15251156605 0020051 0 ustar 00 <?php /** * Main functions to start Fraud & Risk tools class. * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; /** * Returns the main instance of Fraud and Risk tools. * * @return WCPay\Fraud_Prevention\Fraud_Risk_Tools */ function WC_Payments_Fraud_Risk_Tools() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid return WCPay\Fraud_Prevention\Fraud_Risk_Tools::instance(); } add_action( 'plugins_loaded', 'WC_Payments_Fraud_Risk_Tools', 12 ); includes/fraud-prevention/class-fraud-prevention-service.php 0000777 00000010762 15251156605 0020427 0 ustar 00 <?php /** * Class Fraud_Prevention_Service * * @package WCPay\Fraud_Prevention */ namespace WCPay\Fraud_Prevention; use WC_Payment_Gateway_WCPay; use WC_Payments; /** * Class Fraud_Prevention_Service */ class Fraud_Prevention_Service { const TOKEN_NAME = 'wcpay-fraud-prevention-token'; /** * Singleton instance. * * @var Fraud_Prevention_Service */ private static $instance; /** * Session instance. * * @var \WC_Session */ private $session; /** * Instance of WC_Payment_Gateway_WCPay. * * @var WC_Payment_Gateway_WCPay */ private $wcpay_gateway; /** * Fraud_Prevention_Service constructor. * * @param \WC_Session $session Session instance. * @param WC_Payment_Gateway_WCPay $wcpay_gateway Instance of WC_Payment_Gateway_WCPay. */ public function __construct( \WC_Session $session, WC_Payment_Gateway_WCPay $wcpay_gateway ) { $this->session = $session; $this->wcpay_gateway = $wcpay_gateway; } /** * Returns singleton instance. * * @param null $session Session instance. * @param null $gateway WC_Payment_Gateway_WCPay instance. * @return Fraud_Prevention_Service */ public static function get_instance( $session = null, $gateway = null ): self { if ( null === self::$instance ) { self::$instance = new self( $session ?? WC()->session, $gateway ?? WC_Payments::get_gateway() ); } return self::$instance; } /** * Appends the fraud prevention token to the JS context if the protection is enabled, and a session exists. * This token will also be used by express checkouts. * * @return void */ public static function maybe_append_fraud_prevention_token() { if ( wp_script_is( self::TOKEN_NAME, 'enqueued' ) ) { return; } // Check session first before trying to append the token. if ( ! WC()->session ) { return; } $instance = self::get_instance(); // Don't add the token if the prevention is not enabled. if ( ! $instance->is_enabled() ) { return; } // Don't add the token if the user isn't on the cart, checkout, product or pay for order page. // Checking the product and cart page too because the user can pay quickly via the payment buttons on that page. if ( ! is_checkout() && ! has_block( 'woocommerce/checkout' ) && ! is_cart() && ! is_product() && ! $instance->is_pay_for_order_page() ) { return; } wp_register_script( self::TOKEN_NAME, '', [], time(), true ); wp_enqueue_script( self::TOKEN_NAME ); // Add the fraud prevention token to the checkout configuration. wp_add_inline_script( self::TOKEN_NAME, "window.wcpayFraudPreventionToken = '" . esc_js( $instance->get_token() ) . "';", 'after' ); } /** * Checks if this is the Pay for Order page. * * @return bool */ public function is_pay_for_order_page() { return is_checkout() && isset( $_GET['pay_for_order'] ); // phpcs:ignore WordPress.Security.NonceVerification } /** * Sets a instance to be used in request cycle. * Introduced primarily for supporting unit tests. * * @param Fraud_Prevention_Service|null $instance Instance of self. */ public static function set_instance( ?self $instance = null ) { self::$instance = $instance; } /** * Checks if fraud prevention feature is enabled for the account. * * @return bool */ public function is_enabled(): bool { return $this->wcpay_gateway->is_card_testing_protection_eligible(); } /** * Returns current valid token. * * For the first page load generates the token, * for consecutive loads - takes from session. * * @return string */ public function get_token(): string { $fraud_prevention_token = $this->session->get( self::TOKEN_NAME ); if ( ! $fraud_prevention_token ) { $fraud_prevention_token = $this->regenerate_token(); } return $fraud_prevention_token; } /** * Generates a new token, persists in session and returns for immediate use. * * @return string */ public function regenerate_token(): string { $token = wp_generate_password( 16, false ); $this->session->set( self::TOKEN_NAME, $token ); return $token; } /** * Verifies the token against POST data. * * @param string|null $token Token sent in request. * @return bool */ public function verify_token( ?string $token = null ): bool { $session_token = $this->session->get( self::TOKEN_NAME ); // Check if the tokens are both strings. if ( ! is_string( $session_token ) || ! is_string( $token ) ) { return false; } // Compare the hashes to check request validity. return hash_equals( $session_token, $token ); } } includes/class-wc-payments-blocks-payment-method.php 0000777 00000005613 15251156605 0016663 0 ustar 00 <?php /** * Class WC_Payments_Blocks_Payment_Method * * @package WooCommerce\Payments */ use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType; use WCPay\Fraud_Prevention\Fraud_Prevention_Service; use WCPay\WC_Payments_Checkout; use WCPay\WooPay\WooPay_Utilities; /** * The payment method, which allows the gateway to work with WooCommerce Blocks. */ class WC_Payments_Blocks_Payment_Method extends AbstractPaymentMethodType { /** * The gateway instance. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * WC Payments Checkout * * @var WC_Payments_Checkout */ private $wc_payments_checkout; /** * Initializes the class. */ public function initialize() { $this->name = WC_Payment_Gateway_WCPay::GATEWAY_ID; $this->gateway = WC_Payments::get_gateway(); $this->wc_payments_checkout = WC_Payments::get_wc_payments_checkout(); } /** * Checks whether the gateway is active. * * @return boolean True when active. */ public function is_active() { return $this->gateway->is_available(); } /** * Defines all scripts, necessary for the payment method. * * @return string[] A list of script handles. */ public function get_payment_method_script_handles() { if ( ( is_cart() || is_checkout() || is_product() || has_block( 'woocommerce/checkout' ) || has_block( 'woocommerce/cart' ) || is_admin() ) ) { WC_Payments_Utils::enqueue_style( 'wc-blocks-checkout-style', plugins_url( 'dist/blocks-checkout.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'dist/checkout.css' ), 'all' ); } wp_register_script( 'stripe', 'https://js.stripe.com/v3/', [], '3.0', true ); WC_Payments::register_script_with_dependencies( 'WCPAY_BLOCKS_CHECKOUT', 'dist/blocks-checkout', [ 'stripe' ] ); wp_set_script_translations( 'WCPAY_BLOCKS_CHECKOUT', 'woocommerce-payments' ); Fraud_Prevention_Service::maybe_append_fraud_prevention_token(); return [ 'WCPAY_BLOCKS_CHECKOUT' ]; } /** * Loads the data about the gateway, which will be exposed in JavaScript. * * @return array An associative array, containing all necessary values. */ public function get_payment_method_data() { $is_woopay_eligible = WC_Payments_Features::is_woopay_eligible(); // Feature flag. $is_woopay_enabled = 'yes' === $this->gateway->get_option( 'platform_checkout', 'no' ); $woopay_config = []; if ( $is_woopay_eligible && $is_woopay_enabled ) { $woopay_config = [ 'woopayHost' => WooPay_Utilities::get_woopay_url(), ]; } return array_merge( [ 'title' => $this->gateway->get_option( 'title', '' ), 'description' => $this->gateway->get_option( 'description', '' ), 'is_admin' => is_admin(), // Used to display payment method preview in wp-admin. ], $woopay_config, $this->wc_payments_checkout->get_payment_fields_js_config() ); } } includes/class-wc-payments-tasks.php 0000777 00000002735 15251156605 0013604 0 ustar 00 <?php /** * WC_Payments_Tasks class * * @package WooCommerce\Payments\Tasks */ use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists; use WooCommerce\Payments\Tasks\WC_Payments_Task_Disputes; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Hooks into WC TaskLists to display WCPay tasks. */ class WC_Payments_Tasks { /** * WC_Payments_Admin_Tasks constructor. */ public static function init() { // As WooCommerce Onboarding tasks need to hook into 'init' and requires an API call. // We only add this task for users who can manage_woocommerce / view the task. if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } add_action( 'init', [ __CLASS__, 'add_task_disputes_need_response' ] ); } /** * Adds a task to the WC 'Things to do next' task list the if disputes awaiting response. */ public static function add_task_disputes_need_response() { $account_service = WC_Payments::get_account_service(); // The task is not required if the account is not connected, under review, or rejected. if ( ! $account_service || ! $account_service->is_stripe_account_valid() || $account_service->is_account_under_review() || $account_service->is_account_rejected() ) { return; } include_once WCPAY_ABSPATH . 'includes/admin/tasks/class-wc-payments-task-disputes.php'; // 'extended' = 'Things to do next' task list on WooCommerce > Home. TaskLists::add_task( 'extended', new WC_Payments_Task_Disputes() ); } } includes/class-wc-payments-payment-method-service.php 0000777 00000006350 15251156605 0017045 0 ustar 00 <?php /** * Class WC_Payments_Payment_Method_Service * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Exceptions\API_Exception; use WCPay\Logger; /** * Class handling payment method-related functionality. * * Note that this is regarding Stripe-like PaymentMethod objects, not payment methods (p24, card, etc.). */ class WC_Payments_Payment_Method_Service { /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * Order service. * * @var WC_Payments_Order_Service */ private $order_service; /** * Constructor for WC_Payments_Payment_Method_Service. * * @param WC_Payments_API_Client $payments_api_client Client for making requests to the WooCommerce Payments API. * @param WC_Payments_Order_Service $order_service Order service. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Order_Service $order_service ) { $this->payments_api_client = $payments_api_client; $this->order_service = $order_service; } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_filter( 'wc_order_payment_card_info', [ $this, 'get_card_info' ], 10, 2 ); } /** * Prepare card info for an order. * * @param array $card_info The card info. * @param WC_Order $order The order. * @return array The card info. */ public function get_card_info( $card_info, WC_Order $order ) { if ( WC_Payment_Gateway_WCPay::GATEWAY_ID !== $order->get_payment_method() ) { return $card_info; } $payment_method_details = $this->order_service->get_payment_method_details( $order ); if ( ! $payment_method_details ) { $payment_method_id = $order->get_meta( '_payment_method_id' ); if ( ! $payment_method_id ) { return $card_info; } try { $payment_method_details = $this->payments_api_client->get_payment_method( $payment_method_id ); } catch ( API_Exception $ex ) { Logger::error( sprintf( 'Retrieving info for payment method for order %s: %s', $order->get_id(), $ex->getMessage() ) ); return $card_info; } // Cache payment method details. $this->order_service->store_payment_method_details( $order, $payment_method_details ); } $card_info = []; if ( isset( $payment_method_details['type'], $payment_method_details[ $payment_method_details['type'] ] ) ) { $details = $payment_method_details[ $payment_method_details['type'] ]; switch ( $payment_method_details['type'] ) { case 'card': default: $card_info['brand'] = $details['brand'] ?? ''; $card_info['last4'] = $details['last4'] ?? ''; break; case 'card_present': case 'interac_present': $card_info['brand'] = $details['brand'] ?? ''; $card_info['last4'] = $details['last4'] ?? ''; $card_info['account_type'] = $details['receipt']['account_type'] ?? ''; $card_info['aid'] = $details['receipt']['dedicated_file_name'] ?? ''; $card_info['app_name'] = $details['receipt']['application_preferred_name'] ?? ''; break; } } return array_map( 'sanitize_text_field', $card_info ); } } includes/class-wc-payment-token-wcpay-sepa.php 0000777 00000003763 15251156605 0015465 0 ustar 00 <?php /** * Class WC_Payment_Token_WCPay_SEPA * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WooCommerce Stripe SEPA Direct Debit Payment Token. * * Representation of a payment token for SEPA. * * @class WC_Payment_Token_WCPay_SEPA */ class WC_Payment_Token_WCPay_SEPA extends WC_Payment_Token { /** * Class Constant so other code can be unambiguous. * * @type string */ const TYPE = 'wcpay_sepa'; /** * The payment method type of this token. * * @var string */ protected $type = self::TYPE; /** * Stores SEPA payment token data. * * @var array */ protected $extra_data = [ 'last4' => '', ]; /** * Get type to display to user. * * @param string $deprecated Deprecated since WooCommerce 3.0. * @return string */ public function get_display_name( $deprecated = '' ) { $display = sprintf( /* translators: last 4 digits of IBAN account */ __( 'SEPA IBAN ending in %s', 'woocommerce-payments' ), $this->get_last4() ); return $display; } /** * Hook prefix. */ protected function get_hook_prefix() { return 'woocommerce_payments_token_wcpay_sepa_get_'; } /** * Validate SEPA payment tokens. * * These fields are required by all SEPA payment tokens: * last4 - string Last 4 digits of the iBAN * * @return boolean True if the passed data is valid */ public function validate() { if ( false === parent::validate() ) { return false; } if ( ! $this->get_last4( 'edit' ) ) { return false; } return true; } /** * Returns the last four digits. * * @param string $context What the value is for. Valid values are view and edit. * @return string Last 4 digits */ public function get_last4( $context = 'view' ) { return $this->get_prop( 'last4', $context ); } /** * Set the last four digits. * * @param string $last4 SEPA Debit last four digits. */ public function set_last4( $last4 ) { $this->set_prop( 'last4', $last4 ); } } includes/inline-script-payloads/class-woo-payments-payment-method-definitions.php 0000777 00000003043 15251156605 0024501 0 ustar 00 <?php /** * Class Woo_Payments_Payment_Method_Definitions * * @package WCPay\Inline_Script_Payloads */ namespace WCPay\Inline_Script_Payloads; /** * Class Woo_Payments_Payment_Method_Definitions. * To be only used in the `wp-admin` area, to provide the configuration for the payment methods to the JS files. * Use it with `wp_add_inline_script`. * * The advantage of this class with the `__toString` magic method is that the JS payload is lazily calculated * _only_ when the dependent script is actually loaded to the page. */ class Woo_Payments_Payment_Method_Definitions { /** * Lazily calculates and returns the string that will be added to the page by the `wp_add_inline_script` function. * * @return string */ public function __toString() { $account = \WC_Payments::get_account_service()->get_cached_account_data(); $account_country = isset( $account['country'] ) ? strtoupper( $account['country'] ) : ''; $payment_method_map = \WC_Payments::get_payment_method_map(); $payment_method_information_object = []; foreach ( $payment_method_map as $id => $payment_method ) { $payment_method_information_object[ $id ] = $payment_method->get_payment_method_information_object( $account_country ); } $payment_method_information_object = rawurlencode( wp_json_encode( $payment_method_information_object ) ); return " window.wooPaymentsPaymentMethodDefinitions = JSON.parse( decodeURIComponent( '" . esc_js( $payment_method_information_object ) . "' ) ); "; } } includes/inline-script-payloads/class-woo-payments-payment-methods-config.php 0000777 00000002030 15251156605 0023611 0 ustar 00 <?php /** * Class Woo_Payments_Payment_Methods_Config * * @package WCPay\Inline_Script_Payloads */ namespace WCPay\Inline_Script_Payloads; /** * Class Woo_Payments_Payment_Methods_Config. * This should be used anywhere the payment methods config is needed when not available from the `wcpayConfig` object. * Use it with `wp_add_inline_script`. * * The advantage of this class with the `__toString` magic method is that the JS payload is lazily calculated * _only_ when the dependent script is actually loaded to the page. */ class Woo_Payments_Payment_Methods_Config { /** * Lazily calculates and returns the string that will be added to the page by the `wp_add_inline_script` function. * * @return string */ public function __toString() { $payment_methods_config = rawurlencode( wp_json_encode( \WC_Payments::get_wc_payments_checkout()->get_all_payment_method_config() ) ); return " window.wooPaymentsPaymentMethodsConfig = JSON.parse( decodeURIComponent( '" . esc_js( $payment_methods_config ) . "' ) ); "; } } includes/class-duplicate-payment-prevention-service.php 0000777 00000020473 15251156605 0017463 0 ustar 00 <?php /** * Class WC_Payments_Duplicate_Payment_Prevention_Service * * @package WooCommerce\Payments */ namespace WCPay; use Exception; use WC_Order; use WC_Payment_Gateway_WCPay; use WC_Payments_Order_Service; use WCPay\Constants\Intent_Status; use WCPay\Core\Server\Request\Get_Intention; use WCPay\Exceptions\Process_Payment_Exception; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Used for methods, which detect existing payments or payment intents, * and prevent creating duplicate payments. */ class Duplicate_Payment_Prevention_Service { /** * Key name for saving the current processing order_id to WC Session with the purpose * of preventing duplicate payments in a single order. * * @type string */ const SESSION_KEY_PROCESSING_ORDER = 'wcpay_processing_order'; /** * Flag to indicate that a previous order with the same cart content has already paid. * * @type string */ const FLAG_PREVIOUS_ORDER_PAID = 'wcpay_paid_for_previous_order'; /** * Flag to indicate that a previous intention attached to the order was successful. */ const FLAG_PREVIOUS_SUCCESSFUL_INTENT = 'wcpay_previous_successful_intent'; /** * WC_Payments_Order_Service instance. * * @var WC_Payments_Order_Service */ protected $order_service; /** * Gateway instance. * * @var WC_Payment_Gateway_WCPay */ protected $gateway; /** * Initializes all dependencies and hooks, related to the service. * * @param WC_Payment_Gateway_WCPay $gateway The main gateway. * @param WC_Payments_Order_Service $order_service The order service instance. */ public function init( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Order_Service $order_service ) { $this->gateway = $gateway; $this->order_service = $order_service; } /** * Checks if the attached payment intent was successful for the current order. * * @param WC_Order $order Current order to check. * * @return array|void A successful response in case the attached intent was successful, null if none. * @throws Process_Payment_Exception When order amount doesn't match the charged amount. */ public function check_payment_intent_attached_to_order_succeeded( WC_Order $order ) { $intent_id = (string) $order->get_meta( '_intent_id', true ); if ( empty( $intent_id ) ) { return; } // We only care about payment intent. $is_payment_intent = 'pi_' === substr( $intent_id, 0, 3 ); if ( ! $is_payment_intent ) { return; } try { $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); /** @var \WC_Payments_API_Payment_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort $intent = $request->send(); $intent_status = $intent->get_status(); } catch ( Exception $e ) { Logger::error( 'Failed to fetch attached payment intent: ' . $e ); return; } if ( ! $intent->is_authorized() ) { return; } $intent_meta_order_id_raw = $intent->get_metadata()['order_id'] ?? ''; $intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0; $intent_meta_order_number_raw = $intent->get_metadata()['order_number'] ?? ''; $intent_meta_order_number = is_numeric( $intent_meta_order_number_raw ) ? intval( $intent_meta_order_number_raw ) : 0; $paid_on_woopay = filter_var( $intent->get_metadata()['paid_on_woopay'] ?? false, FILTER_VALIDATE_BOOLEAN ); $is_woopay_order = $order->get_id() === $intent_meta_order_number; if ( ! ( $paid_on_woopay && $is_woopay_order ) && $intent_meta_order_id !== $order->get_id() ) { return; } if ( Intent_Status::SUCCEEDED === $intent_status ) { $this->remove_session_processing_order( $order->get_id() ); } // Check if the order amount matches the charged amount. $order_total_in_cents = \WC_Payments_Utils::prepare_amount( $order->get_total(), $order->get_currency() ); $charged_amount = $intent->get_amount(); // If amounts don't match, this indicates the order was modified after payment. // Throw an exception to prevent duplicate payment and inform the customer. if ( $order_total_in_cents !== $charged_amount ) { // Throw exception with customer-friendly message. throw new Process_Payment_Exception( sprintf( /* translators: 1: charged amount, 2: current order total */ __( 'This order was already paid for %1$s, but the order total has since changed to %2$s, so we prevented an overpayment. Please create a new order for any additional items.', 'woocommerce-payments' ), wc_price( \WC_Payments_Utils::interpret_stripe_amount( $charged_amount, $order->get_currency() ), [ 'currency' => $order->get_currency() ] ), wc_price( \WC_Payments_Utils::interpret_stripe_amount( $order_total_in_cents, $order->get_currency() ), [ 'currency' => $order->get_currency() ] ) ), 'duplicate_payment_amount_mismatch' ); } // Amounts match, proceed with normal status update. $this->order_service->update_order_status_from_intent( $order, $intent ); $return_url = $this->gateway->get_return_url( $order ); $return_url = add_query_arg( self::FLAG_PREVIOUS_SUCCESSFUL_INTENT, 'yes', $return_url ); return [ // nosemgrep: audit.php.wp.security.xss.query-arg -- https://woocommerce.github.io/code-reference/classes/WC-Payment-Gateway.html#method_get_return_url is passed in. 'result' => 'success', 'redirect' => $return_url, ]; } /** * Checks if the current order has the same content with the session processing order, which was already paid (ex. by a webhook). * * @param WC_Order $current_order Current order in process_payment. * * @return array|void A successful response in case the session processing order was paid, null if none. */ public function check_against_session_processing_order( WC_Order $current_order ) { $session_order_id = $this->get_session_processing_order(); if ( null === $session_order_id ) { return; } $session_order = wc_get_order( $session_order_id ); if ( ! is_a( $session_order, 'WC_Order' ) ) { return; } if ( $current_order->get_cart_hash() !== $session_order->get_cart_hash() ) { return; } if ( ! $session_order->has_status( wc_get_is_paid_statuses() ) ) { return; } if ( ! $current_order->has_status( wc_get_is_pending_statuses() ) ) { return; } if ( $session_order->get_id() === $current_order->get_id() ) { return; } if ( $session_order->get_customer_id() !== $current_order->get_customer_id() ) { return; } $session_order->add_order_note( sprintf( /* translators: order ID integer number */ __( 'WooCommerce Payments: detected and deleted order ID %d, which has duplicate cart content with this order.', 'woocommerce-payments' ), $current_order->get_id() ) ); $current_order->delete(); $this->remove_session_processing_order( $session_order_id ); $return_url = $this->gateway->get_return_url( $session_order ); $return_url = add_query_arg( self::FLAG_PREVIOUS_ORDER_PAID, 'yes', $return_url ); return [ // nosemgrep: audit.php.wp.security.xss.query-arg -- https://woocommerce.github.io/code-reference/classes/WC-Payment-Gateway.html#method_get_return_url is passed in. 'result' => 'success', 'redirect' => $return_url, ]; } /** * Update the processing order ID for the current session. * * @param int $order_id Order ID. * * @return void */ public function maybe_update_session_processing_order( int $order_id ) { if ( WC()->session ) { WC()->session->set( self::SESSION_KEY_PROCESSING_ORDER, $order_id ); } } /** * Remove the provided order ID from the current session if it matches with the ID in the session. * * @param int $order_id Order ID to remove from the session. * * @return void */ public function remove_session_processing_order( int $order_id ) { $current_session_id = $this->get_session_processing_order(); if ( $order_id === $current_session_id && WC()->session ) { WC()->session->set( self::SESSION_KEY_PROCESSING_ORDER, null ); } } /** * Get the processing order ID for the current session. * * @return integer|null Order ID. Null if the value is not set. */ protected function get_session_processing_order() { $session = WC()->session; if ( null === $session ) { return null; } $val = $session->get( self::SESSION_KEY_PROCESSING_ORDER ); return null === $val ? null : absint( $val ); } } includes/admin/class-wc-rest-payments-charges-controller.php 0000777 00000007371 15251156605 0020320 0 ustar 00 <?php /** * Class WC_REST_Payments_Charges_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request\Get_Charge; use WCPay\Exceptions\API_Exception; defined( 'ABSPATH' ) || exit; /** * REST controller for charges. */ class WC_REST_Payments_Charges_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/charges'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<charge_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_charge' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/order/(?P<order_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'generate_charge_from_order' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve charge to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_charge( $request ) { $charge_id = $request->get_param( 'charge_id' ); try { $wcpay_request = Get_Charge::create( $charge_id ); $charge = $wcpay_request->send(); } catch ( API_Exception $e ) { return rest_ensure_response( new WP_Error( 'wcpay_get_charge', $e->getMessage() ) ); } return rest_ensure_response( $charge ); } /** * Generates a charge-like object from an order. * * @param WP_REST_Request $request Full data about the request. * * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function generate_charge_from_order( $request ) { $order_id = $request['order_id']; $order = wc_get_order( $order_id ); if ( false === $order ) { return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] ); } $currency = $order->get_currency(); $amount = WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ); $billing_details = WC_Payments::get_order_service()->get_billing_data_from_order( $order ); // TODO: Inject order_service after #7464 is fixed. $date_created = $order->get_date_created(); $intent_id = $order->get_meta( '_intent_id' ); $intent_status = $order->get_meta( '_intent_status' ); $charge = [ 'id' => $order->get_id(), 'amount' => $amount, 'amount_captured' => 0, 'amount_refunded' => 0, 'application_fee_amount' => 0, 'balance_transaction' => [ 'currency' => $currency, 'amount' => $amount, 'fee' => 0, ], 'billing_details' => $billing_details, 'created' => $date_created ? $date_created->getTimestamp() : null, 'currency' => $currency, 'disputed' => false, 'outcome' => false, 'order' => $this->api_client->build_order_info( $order ), 'paid' => false, 'paydown' => null, 'payment_intent' => ! empty( $intent_id ) ? $intent_id : null, 'payment_method_details' => [ 'card' => [ 'country' => $order->get_billing_country(), 'checks' => [], 'network' => '', ], 'type' => 'card', ], 'refunded' => false, 'refunds' => null, 'status' => ! empty( $intent_status ) ? $intent_status : $order->get_status(), ]; $charge = $this->api_client->add_formatted_address_to_charge_object( $charge ); return rest_ensure_response( $charge ); } } includes/admin/class-wc-rest-woopay-session-controller.php 0000777 00000005146 15251156605 0020043 0 ustar 00 <?php /** * Class WC_REST_WooPay_Session_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; use WCPay\WooPay\WooPay_Session; use Automattic\Jetpack\Connection\Rest_Authentication; use WCPay\Logger; /** * REST controller to check get WooPay extension data for user. */ class WC_REST_WooPay_Session_Controller extends WP_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'payments/woopay'; /** * Endpoint path. * * @var string */ protected $rest_base = 'session'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_session_data' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'email' => [ 'type' => 'string', 'format' => 'email', 'required' => true, ], ], ] ); } /** * Retrieve WooPay session data. * * @param WP_REST_Request $request Full details about the request. * * @return WP_Error|WP_REST_Response The initial session request data. */ public function get_session_data( WP_REST_Request $request ): object { try { $response = WooPay_Session::get_init_session_request( null, null, null, $request ); return rest_ensure_response( $response ); } catch ( Exception $e ) { $error = new WP_Error( 'wcpay_server_error', $e->getMessage(), [ 'status' => 400 ] ); Logger::log( 'Error validating cart token from WooPay request: ' . $e->getMessage() ); return rest_convert_error_to_response( $error ); } } /** * Check permission confirms that the request is from WooPay. * * @return bool True if request is from WooPay and has a valid signature. */ public function check_permission() { return $this->is_request_from_woopay() && $this->has_valid_request_signature(); } /** * Returns true if the request that's currently being processed is signed with the blog token. * * @return bool True if the request signature is valid. */ private function has_valid_request_signature(): bool { return apply_filters( 'wcpay_woopay_is_signed_with_blog_token', Rest_Authentication::is_signed_with_blog_token() ); } /** * Returns true if the request that's currently being processed is from WooPay, false * otherwise. * * @return bool True if request is from WooPay. */ private function is_request_from_woopay(): bool { return isset( $_SERVER['HTTP_USER_AGENT'] ) && 'WooPay' === $_SERVER['HTTP_USER_AGENT']; } } includes/admin/class-wc-rest-payments-documents-controller.php 0000777 00000010367 15251156605 0020704 0 ustar 00 <?php /** * Class WC_REST_Payments_Documents_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request\List_Documents; use WCPay\Exceptions\API_Exception; defined( 'ABSPATH' ) || exit; /** * REST controller for documents. */ class WC_REST_Payments_Documents_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/documents'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_documents' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/summary', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_documents_summary' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<document_id>[\w-]+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_document' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve documents to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_documents( $request ) { $wcpay_request = List_Documents::from_rest_request( $request ); return $wcpay_request->handle_rest_request(); } /** * Retrieve documents summary to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_documents_summary( $request ) { $filters = $this->get_documents_filters( $request ); return $this->forward_request( 'get_documents_summary', [ $filters ] ); } /** * Retrieve and serve a document for API requests. * This method serves the document directly and halts execution, skipping the REST return * and preventing additional data to be sent. * * @param WP_REST_Request $request Full data about the request. */ public function get_document( $request ) { $response = []; $document_id = $request->get_param( 'document_id' ); try { $response = $this->api_client->get_document( $document_id ); } catch ( API_Exception $e ) { $message = sprintf( /* translators: %1: The document ID. %2: The error message.*/ esc_html__( 'There was an error accessing document %1$s. %2$s', 'woocommerce-payments' ), $document_id, $e->getMessage() ); wp_die( esc_html( $message ), '', (int) $e->get_http_code() ); } // WooCommerce core only includes Tracks in admin, not the REST API, so we need to use this wc_admin method // that includes WC_Tracks in case it's not loaded. if ( function_exists( 'wc_admin_record_tracks_event' ) ) { wc_admin_record_tracks_event( 'wcpay_document_downloaded', [ 'document_id' => $document_id, 'mode' => WC_Payments::mode()->is_test() ? 'test' : 'live', ] ); } // Set the headers to match what was returned from the server. if ( ! headers_sent() ) { nocache_headers(); status_header( $response['response']['code'], $response['response']['message'] ?? '' ); header( 'Content-Type: ' . $response['headers']['content-type'] ); header( 'Content-Disposition: ' . $response['headers']['content-disposition'] ?? '' ); } // We should output the server's file without escaping. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $response['body']; exit; } /** * Extract documents filters from request * * @param WP_REST_Request $request Full data about the request. */ private function get_documents_filters( $request ) { return array_filter( [ 'match' => $request->get_param( 'match' ), 'date_before' => $request->get_param( 'date_before' ), 'date_after' => $request->get_param( 'date_after' ), 'date_between' => $request->get_param( 'date_between' ), 'type_is' => $request->get_param( 'type_is' ), 'type_is_not' => $request->get_param( 'type_is_not' ), ], static function ( $filter ) { return null !== $filter; } ); } } includes/admin/class-wc-rest-payments-terminal-locations-controller.php 0000777 00000025062 15251156605 0022505 0 ustar 00 <?php /** * Class WC_REST_Payments_Terminal_Locations_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; use WCPay\Core\Server\Request; use WCPay\Exceptions\API_Exception; /** * REST controller for account details and status. */ class WC_REST_Payments_Terminal_Locations_Controller extends WC_Payments_REST_Controller { const STORE_LOCATIONS_TRANSIENT_KEY = 'wcpay_store_terminal_locations'; /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/terminal/locations'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, $this->rest_base . '/store', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_store_location' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<location_id>\w+)', [ 'methods' => WP_REST_Server::DELETABLE, 'callback' => [ $this, 'delete_location' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<location_id>\w+)', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'update_location' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'display_name' => [ 'type' => 'string', 'required' => false, ], 'address' => [ 'type' => 'object', 'required' => false, ], ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<location_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_location' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_location' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'display_name' => [ 'type' => 'string', 'required' => true, ], 'address' => [ 'type' => 'object', 'required' => true, ], ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_all_locations' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Get store terminal location. * * @param WP_REST_Request $request Request object. * * @return WP_REST_Response|WP_Error */ public function get_store_location( $request ) { $store_address = WC()->countries; $location_address = array_filter( [ 'city' => $store_address->get_base_city(), 'country' => $store_address->get_base_country(), 'line1' => $store_address->get_base_address(), 'line2' => $store_address->get_base_address_2(), 'postal_code' => $store_address->get_base_postcode(), 'state' => $store_address->get_base_state(), ] ); // Special handling for Puerto Rico - treat as US state rather than country. if ( 'PR' === $location_address['country'] ) { $location_address['country'] = 'US'; $location_address['state'] = 'PR'; } // If address is not populated, emit an error and specify the URL where this can be done. // See also https://tosbourn.com/list-of-countries-without-a-postcode/ when launching in new countries. $is_address_populated = isset( $location_address['country'], $location_address['city'], $location_address['postal_code'], $location_address['line1'] ); if ( ! $is_address_populated ) { return rest_ensure_response( new \WP_Error( 'store_address_is_incomplete', admin_url( add_query_arg( [ 'page' => 'wc-settings', 'tab' => 'general', ], 'admin.php' ) ) ) ); } try { // Check the existing locations to see if one of them matches the store. // Originally we picked `get_bloginfo` for generating names, but later switched to `site_url` for max immutability. $store_hostname = str_replace( [ 'https://', 'http://' ], '', get_site_url() ); $possible_names = [ get_bloginfo(), $store_hostname ]; foreach ( $this->fetch_locations() as $location ) { if ( in_array( $location['display_name'], $possible_names, true ) ) { $matching_address_fields = array_intersect( $location['address'], $location_address ); if ( count( $matching_address_fields ) === count( $location_address ) ) { return rest_ensure_response( $this->extract_location_fields( $location ) ); } } } // If the location is missing, Create a new one and actualize the transient. $location = $this->api_client->create_terminal_location( $store_hostname, $location_address ); $this->reload_locations(); return rest_ensure_response( $this->extract_location_fields( $location ) ); } catch ( API_Exception $e ) { $error = new WP_Error( $e->get_error_code(), $e->getMessage() ); // Stripe will return a 400 for incorrect city, state, or country. Ideally, we should return // a more appropriate error code like 'store_address_is_incorrect', but that will break older mobile app clients. // Until we have a more granular versioning support for WCPay REST endpoints, this is the best we can do. if ( 'invalid_request_error' === $e->get_error_code() ) { $error = new WP_Error( 'store_address_is_incomplete', admin_url( add_query_arg( [ 'page' => 'wc-settings', 'tab' => 'general', ], 'admin.php' ) ) ); } return rest_ensure_response( $error ); } } /** * Proxies the delete location request to the server. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function delete_location( $request ) { try { // Delete the location and reload the transient. $location = $this->api_client->delete_terminal_location( $request->get_param( 'location_id' ) ); $this->reload_locations(); return rest_ensure_response( $location ); } catch ( API_Exception $e ) { return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) ); } } /** * Proxies the update location request to the server. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function update_location( $request ) { try { // Update the location and reload the transient. $location = $this->api_client->update_terminal_location( $request->get_param( 'location_id' ), $request['display_name'], $request['address'] ); $this->reload_locations(); return rest_ensure_response( $this->extract_location_fields( $location ) ); } catch ( API_Exception $e ) { return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) ); } } /** * Proxies the get location request to the server. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function get_location( $request ) { try { // Check if the location is already in the transient. $location_id = $request->get_param( 'location_id' ); foreach ( $this->fetch_locations() as $location ) { if ( $location['id'] === $location_id ) { return rest_ensure_response( $this->extract_location_fields( $location ) ); } } // If the location is missing, fetch it individually and reload the transient. $request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API, $location_id ); $request->assign_hook( 'wcpay_get_terminal_location' ); $location = $request->send(); $this->reload_locations(); return rest_ensure_response( $this->extract_location_fields( $location ) ); } catch ( API_Exception $e ) { return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) ); } } /** * Proxies the create location request to the server. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function create_location( $request ) { try { // Create location and reload the transient. $location = $this->api_client->create_terminal_location( $request['display_name'], $request['address'] ); $this->reload_locations(); return rest_ensure_response( $this->extract_location_fields( $location ) ); } catch ( API_Exception $e ) { return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) ); } } /** * Proxies the get all locations request to the server. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function get_all_locations( $request ) { try { return rest_ensure_response( array_map( [ $this, 'extract_location_fields' ], $this->fetch_locations() ) ); } catch ( API_Exception $e ) { return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) ); } } /** * Extracts the relevant fields from a terminal location object. * * @param array $location The location. * @return array The picked fields from location object. */ private function extract_location_fields( array $location ): array { return [ 'id' => $location['id'], 'address' => $location['address'], 'display_name' => $location['display_name'], 'livemode' => $location['livemode'], ]; } /** * Attempts to read locations from transient and re-populates it if needed. * * @return array Terminal locations. * @throws API_Exception If request to server fails. */ private function fetch_locations(): array { $locations = get_transient( static::STORE_LOCATIONS_TRANSIENT_KEY ); if ( ! $locations ) { $request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API ); $request->assign_hook( 'wcpay_get_terminal_locations' ); $locations = $request->send(); set_transient( static::STORE_LOCATIONS_TRANSIENT_KEY, $locations, DAY_IN_SECONDS ); } return $locations; } /** * Refreshes the locations stored in transient. * * @return void * @throws API_Exception If request to server fails. */ private function reload_locations() { $request = Request::get( WC_Payments_API_Client::TERMINAL_LOCATIONS_API ); $request->assign_hook( 'wcpay_get_terminal_locations' ); $locations = $request->send(); set_transient( static::STORE_LOCATIONS_TRANSIENT_KEY, $locations, DAY_IN_SECONDS ); } } includes/admin/class-wc-rest-payments-orders-controller.php 0000777 00000055301 15251156605 0020176 0 ustar 00 <?php /** * Class WC_REST_Payments_Orders_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; use WCPay\Core\Server\Request\Create_Intention; use WCPay\Core\Server\Request\Get_Intention; use WCPay\Logger; use WCPay\Constants\Order_Status; use WCPay\Constants\Intent_Status; use WCPay\Constants\Payment_Method; /** * REST controller for order processing. */ class WC_REST_Payments_Orders_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/orders'; /** * Instance of WC_Payment_Gateway_WCPay * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * WC_Payments_Customer_Service instance for working with customer information * * @var WC_Payments_Customer_Service */ private $customer_service; /** * WC_Payments_Order_Service instance for updating order statuses. * * @var WC_Payments_Order_Service */ private $order_service; /** * WC_Payments_REST_Controller constructor. * * @param WC_Payments_API_Client $api_client WooCommerce Payments API client. * @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway. * @param WC_Payments_Customer_Service $customer_service Customer class instance. * @param WC_Payments_Order_Service $order_service Order Service class instance. */ public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service ) { parent::__construct( $api_client ); $this->gateway = $gateway; $this->customer_service = $customer_service; $this->order_service = $order_service; } /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, $this->rest_base . '/(?P<order_id>\w+)/capture_terminal_payment', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'capture_terminal_payment' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'payment_intent_id' => [ 'required' => true, ], ], ] ); register_rest_route( $this->namespace, $this->rest_base . '/(?P<order_id>\w+)/capture_authorization', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'capture_authorization' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'payment_intent_id' => [ 'required' => true, ], ], ] ); register_rest_route( $this->namespace, $this->rest_base . '/(?P<order_id>\w+)/cancel_authorization', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'cancel_authorization' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'payment_intent_id' => [ 'required' => true, ], ], ] ); register_rest_route( $this->namespace, $this->rest_base . '/(?P<order_id>\w+)/create_terminal_intent', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_terminal_intent' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, $this->rest_base . '/(?P<order_id>\d+)/create_customer', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_customer' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Given an intent ID and an order ID, add the intent ID to the order and capture it. * Use-cases: Mobile apps using it for `card_present` and `interac_present` payment types. * * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function capture_terminal_payment( WP_REST_Request $request ) { try { $intent_id = $request['payment_intent_id']; $order_id = $request['order_id']; // Do not process non-existing orders. $order = wc_get_order( $order_id ); if ( false === $order ) { return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] ); } // Do not process orders with refund(s). if ( 0 < $order->get_total_refunded() ) { return new WP_Error( 'wcpay_refunded_order_uncapturable', __( 'Payment cannot be captured for partially or fully refunded orders.', 'woocommerce-payments' ), [ 'status' => 400 ] ); } // Do not process already processed orders to prevent double-charging. $processed_order_intent_statuses = [ Intent_Status::SUCCEEDED, Intent_Status::CANCELED, Intent_Status::PROCESSING, ]; $stored_intent_id = $order->get_meta( WC_Payments_Order_Service::INTENT_ID_META_KEY ); $stored_intent_status = $order->get_meta( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY ); if ( in_array( $stored_intent_status, $processed_order_intent_statuses, true ) || ( $stored_intent_id && $stored_intent_id !== $intent_id ) ) { return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured for completed or processed orders.', 'woocommerce-payments' ), [ 'status' => 409 ] ); } // Do not process intents that can't be captured. $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); $intent = $request->send(); $intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : []; $intent_meta_order_id_raw = $intent_metadata['order_id'] ?? ''; $intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0; if ( $intent_meta_order_id !== $order->get_id() ) { Logger::error( 'Payment capture rejected due to failed validation: order id on intent is incorrect or missing.' ); return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] ); } if ( ! $intent->is_authorized() ) { return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] ); } // Update the order: set the payment method and attach intent attributes. $order->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID ); $order->set_payment_method_title( __( 'WooCommerce In-Person Payments', 'woocommerce-payments' ) ); $this->order_service->attach_intent_info_to_order( $order, $intent ); $this->order_service->update_order_status_from_intent( $order, $intent ); // Certain payments (eg. Interac) are captured on the client-side (mobile app). // The client may send us the captured intent to link it to its WC order. // Doing so via this endpoint is more reliable than depending on the payment_intent.succeeded event. $is_intent_captured = Intent_Status::SUCCEEDED === $intent->get_status(); $result_for_captured_intent = [ 'status' => Intent_Status::SUCCEEDED, 'id' => $intent->get_id(), ]; $result = $is_intent_captured ? $result_for_captured_intent : $this->gateway->capture_charge( $order, false, $intent_metadata ); if ( Intent_Status::SUCCEEDED !== $result['status'] ) { $http_code = $result['http_code'] ?? 502; $extra_details = $result['extra_details'] ?? []; $error_type = $result['error_code'] ?? null; $error_code = 'wcpay_capture_error'; $message = sprintf( // translators: %s: the error message. __( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ), $result['message'] ?? __( 'Unknown error', 'woocommerce-payments' ) ); if ( 'amount_too_small' === $error_type && ! empty( $extra_details ) ) { // Make it easier to parse the error metadata for the mobile apps. $error_code = 'wcpay_capture_error_amount_too_small'; $message = esc_html( wp_json_encode( $extra_details ) ); } return new WP_Error( $error_code, $message, [ 'status' => $http_code ] ); } // Store receipt generation URL for mobile applications in order meta-data. $order->add_meta_data( 'receipt_url', get_rest_url( null, sprintf( '%s/payments/readers/receipts/%s', $this->namespace, $intent->get_id() ) ) ); // Add payment method for future subscription payments. $generated_card = $intent->get_charge()->get_payment_method_details()[ Payment_Method::CARD_PRESENT ]['generated_card'] ?? null; // If we don't get a generated card, e.g. because a digital wallet was used, we can still return that the initial payment was successful. // The subscription will not be activated and customers will need to provide a new payment method for renewals. if ( $generated_card ) { $has_subscriptions = function_exists( 'wcs_order_contains_subscription' ) && function_exists( 'wcs_get_subscriptions_for_order' ) && function_exists( 'wcs_is_manual_renewal_required' ) && wcs_order_contains_subscription( $order_id ); if ( $has_subscriptions ) { $token = WC_Payments::get_token_service()->add_payment_method_to_user( $generated_card, $order->get_user() ); $this->gateway->add_token_to_order( $order, $token ); foreach ( wcs_get_subscriptions_for_order( $order ) as $subscription ) { $subscription->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID ); // Where the setting doesn't force manual renewals, we should turn them off, because we have an auto-renewal token now. if ( ! wcs_is_manual_renewal_required() ) { $subscription->set_requires_manual_renewal( false ); } $subscription->save(); } } } // Actualize order status. $this->order_service->mark_terminal_payment_completed( $order, $intent_id, $result['status'] ); return rest_ensure_response( [ 'status' => $result['status'], 'id' => $result['id'], ] ); } catch ( \Throwable $e ) { Logger::error( 'Failed to capture a terminal payment via REST API: ' . $e ); return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] ); } } /** * Captures an authorization. * Use-cases: Merchants manually capturing a payment when they enable "capture later" option. * * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function capture_authorization( WP_REST_Request $request ) { try { $intent_id = $request['payment_intent_id']; $order_id = $request['order_id']; // Do not process non-existing orders. $order = wc_get_order( $order_id ); if ( false === $order ) { return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] ); } // Do not process orders with refund(s). if ( 0 < $order->get_total_refunded() ) { return new WP_Error( 'wcpay_refunded_order_uncapturable', __( 'Payment cannot be captured for partially or fully refunded orders.', 'woocommerce-payments' ), [ 'status' => 400 ] ); } // Do not process intents that can't be captured. $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); $intent = $request->send(); $intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : []; $intent_meta_order_id_raw = $intent_metadata['order_id'] ?? ''; $intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0; if ( $intent_meta_order_id !== $order->get_id() ) { Logger::error( 'Payment capture rejected due to failed validation: order id on intent is incorrect or missing.' ); return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] ); } if ( ! $intent->is_authorized() ) { return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] ); } $this->add_fraud_outcome_manual_entry( $order, 'approve' ); $result = $this->gateway->capture_charge( $order, true, $intent_metadata ); if ( Intent_Status::SUCCEEDED !== $result['status'] ) { $error_code = $result['error_code'] ?? null; $extra_details = $result['extra_details'] ?? []; return new WP_Error( 'wcpay_capture_error', sprintf( // translators: %s: the error message. __( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ), $result['message'] ?? __( 'Unknown error', 'woocommerce-payments' ) ), [ 'status' => $result['http_code'] ?? 502, 'extra_details' => $extra_details, 'error_type' => $error_code, ] ); } $order->save_meta_data(); return rest_ensure_response( [ 'status' => $result['status'], 'id' => $result['id'], ] ); } catch ( \Throwable $e ) { Logger::error( 'Failed to capture an authorization via REST API: ' . $e ); return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] ); } } /** * Returns customer id from order. Create or update customer if needed. * Use-cases: * - It was used by older versions of our mobile apps to add the customer details to Payment Intents. * - It is used by the apps to set customer details on Payment Intents for an order containing subscriptions. Required for capturing renewal payments off session. * * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function create_customer( $request ) { try { $order_id = $request['order_id']; // Do not process non-existing orders. $order = wc_get_order( $order_id ); if ( false === $order || ! ( $order instanceof WC_Order ) ) { return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] ); } $disallowed_order_statuses = apply_filters( 'wcpay_create_customer_disallowed_order_statuses', [ Order_Status::COMPLETED, Order_Status::CANCELLED, Order_Status::REFUNDED, Order_Status::FAILED, ] ); if ( $order->has_status( $disallowed_order_statuses ) ) { return new WP_Error( 'wcpay_invalid_order_status', __( 'Invalid order status', 'woocommerce-payments' ), [ 'status' => 400 ] ); } $order_user = $order->get_user(); $customer_id = $this->order_service->get_customer_id_for_order( $order ); $customer_data = WC_Payments_Customer_Service::map_customer_data( $order ); $is_guest_customer = false === $order_user; // If the order is created for a registered customer, try extracting it's Stripe customer ID. if ( ! $customer_id && ! $is_guest_customer ) { $customer_id = $this->customer_service->get_customer_id_by_user_id( $order_user->ID ); } $order_user = $is_guest_customer ? new WP_User() : $order_user; $customer_id = $customer_id ? $this->customer_service->update_customer_for_user( $customer_id, $order_user, $customer_data ) : $this->customer_service->create_customer_for_user( $order_user, $customer_data ); $this->order_service->set_customer_id_for_order( $order, $customer_id ); $order->save(); return rest_ensure_response( [ 'id' => $customer_id, ] ); } catch ( \Throwable $e ) { Logger::error( 'Failed to create / update customer from order via REST API: ' . $e ); return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] ); } } /** * Create a new in-person payment intent for the given order ID without confirming it. * Use-cases: Mobile apps using it for `card_present` payment types. (`interac_present` is handled by the apps via Stripe SDK). * * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function create_terminal_intent( $request ) { // Do not process non-existing orders. $order = wc_get_order( $request['order_id'] ); if ( false === $order ) { return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] ); } try { $currency = strtolower( $order->get_currency() ); $customer_id = $request->get_param( 'customer_id' ); $metadata = $request->get_param( 'metadata' ) ?? []; $metadata['order_number'] = $order->get_order_number(); $wcpay_server_request = Create_Intention::create(); $wcpay_server_request->set_currency_code( $currency ); $wcpay_server_request->set_amount( WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ) ); if ( $customer_id ) { $wcpay_server_request->set_customer( $customer_id ); } $wcpay_server_request->set_metadata( $metadata ); $wcpay_server_request->set_payment_method_types( $this->get_terminal_intent_payment_method( $request ) ); $wcpay_server_request->set_capture_method( 'manual' === $this->get_terminal_intent_capture_method( $request ) ); $wcpay_server_request->set_hook_args( $order ); $intent = $wcpay_server_request->send(); return rest_ensure_response( [ 'id' => ! empty( $intent ) ? $intent->get_id() : null, ] ); } catch ( \Throwable $e ) { Logger::error( 'Failed to create an intention via REST API: ' . $e ); return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] ); } } /** * Return terminal intent payment method array based on payment methods request. * * @param WP_REST_Request $request Request object. * @param array $default_value - default value. * * @return array * @throws \Exception */ public function get_terminal_intent_payment_method( $request, array $default_value = [ Payment_Method::CARD_PRESENT ] ): array { $payment_methods = $request->get_param( 'payment_methods' ); if ( null === $payment_methods ) { return $default_value; } if ( ! is_array( $payment_methods ) ) { throw new \Exception( 'Invalid param \'payment_methods\'!' ); } foreach ( $payment_methods as $value ) { if ( ! in_array( $value, Payment_Method::IPP_ALLOWED_PAYMENT_METHODS, true ) ) { throw new \Exception( 'One or more payment methods are not supported!' ); } } return $payment_methods; } /** * Return terminal intent capture method based on capture method request. * * @param WP_REST_Request $request Request object. * @param string $default_value default value. * * @return string * @throws \Exception */ public function get_terminal_intent_capture_method( $request, string $default_value = 'manual' ): string { $capture_method = $request->get_param( 'capture_method' ); if ( null === $capture_method ) { return $default_value; } if ( ! in_array( $capture_method, [ 'manual', 'automatic' ], true ) ) { throw new \Exception( 'Invalid param \'capture_method\'!' ); } return $capture_method; } /** * Cancels an authorization. * Use-cases: Merchants manually canceling when blocking an on hold review by Fraud & Risk tools. * * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function cancel_authorization( WP_REST_Request $request ) { try { $intent_id = $request['payment_intent_id']; $order_id = $request['order_id']; // Do not process non-existing orders. $order = wc_get_order( $order_id ); if ( false === $order ) { return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] ); } // Do not process orders with refund(s). if ( 0 < $order->get_total_refunded() ) { return new WP_Error( 'wcpay_refunded_order_uncapturable', __( 'Payment cannot be canceled for partially or fully refunded orders.', 'woocommerce-payments' ), [ 'status' => 400 ] ); } // Do not process intents that can't be canceled. $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); $intent = $request->send(); $intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : []; $intent_meta_order_id_raw = $intent_metadata['order_id'] ?? ''; $intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0; if ( $intent_meta_order_id !== $order->get_id() ) { Logger::error( 'Payment cancellation rejected due to failed validation: order id on intent is incorrect or missing.' ); return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be canceled', 'woocommerce-payments' ), [ 'status' => 409 ] ); } if ( ! in_array( $intent->get_status(), [ Intent_Status::REQUIRES_CAPTURE ], true ) ) { return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be canceled', 'woocommerce-payments' ), [ 'status' => 409 ] ); } $this->add_fraud_outcome_manual_entry( $order, 'block' ); $result = $this->gateway->cancel_authorization( $order ); if ( Intent_Status::CANCELED !== $result['status'] ) { return new WP_Error( 'wcpay_cancel_error', sprintf( // translators: %s: the error message. __( 'Payment cancel failed to complete with the following message: %s', 'woocommerce-payments' ), $result['message'] ?? __( 'Unknown error', 'woocommerce-payments' ) ), [ 'status' => $result['http_code'] ?? 502 ] ); } $order->save_meta_data(); return rest_ensure_response( [ 'status' => $result['status'], 'id' => $result['id'], ] ); } catch ( \Throwable $e ) { Logger::error( 'Failed to cancel an authorization via REST API: ' . $e ); return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] ); } } /** * Adds the fraud_outcome_manual_entry meta to the order. * * @param \WC_Order $order Order object. * @param string $action User action. */ private function add_fraud_outcome_manual_entry( $order, $action ) { $current_user = wp_get_current_user(); $order->add_meta_data( '_wcpay_fraud_outcome_manual_entry', [ 'type' => 'fraud_outcome_manual_' . $action, 'user' => [ 'id' => $current_user->ID, 'username' => $current_user->user_login, ], 'action' => 'block' === $action ? 'blocked' : 'approved', 'datetime' => time(), ] ); } } includes/admin/class-wc-rest-payments-settings-option-controller.php 0000777 00000007004 15251156605 0022043 0 ustar 00 <?php /** * Class WC_REST_Payments_Settings_Option_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; /** * REST controller for settings options. */ class WC_REST_Payments_Settings_Option_Controller extends WC_Payments_REST_Controller { /** * Map of allowed option names to their accepted value types. * Types: 'bool', 'array', 'string'. * * @var array */ private const ALLOWED_OPTIONS = [ 'wcpay_multi_currency_setup_completed' => 'bool', 'woocommerce_dismissed_todo_tasks' => 'array', 'woocommerce_remind_me_later_todo_tasks' => 'array', 'woocommerce_deleted_todo_tasks' => 'array', 'wcpay_fraud_protection_welcome_tour_dismissed' => 'bool', 'wcpay_onboarding_eligibility_modal_dismissed' => 'bool', 'wcpay_connection_success_modal_dismissed' => 'bool', 'wcpay_next_deposit_notice_dismissed' => 'bool', 'wcpay_duplicate_payment_method_notices_dismissed' => 'bool', 'wcpay_instant_deposit_notice_dismissed' => 'bool', 'wcpay_exit_survey_last_shown' => 'string', ]; /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/settings'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<option_name>[a-zA-Z0-9_-]+)', [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_option' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'option_name' => [ 'required' => true, 'validate_callback' => [ $this, 'validate_option_name' ], ], 'value' => [ 'required' => true, 'validate_callback' => [ $this, 'validate_value' ], ], ], ] ); } /** * Validate the option name. * * @param string $option_name The option name to validate. * @return bool */ public function validate_option_name( string $option_name ): bool { return array_key_exists( $option_name, self::ALLOWED_OPTIONS ); } /** * Validate the value parameter based on the option's expected type. * * @param mixed $value The value to validate. * @param WP_REST_Request $request The request object. * @return bool|WP_Error True if valid, WP_Error if invalid. */ public function validate_value( $value, WP_REST_Request $request ) { $option_name = $request->get_param( 'option_name' ); $expected_type = self::ALLOWED_OPTIONS[ $option_name ] ?? null; $is_valid = false; switch ( $expected_type ) { case 'bool': $is_valid = is_bool( $value ); break; case 'array': $is_valid = is_array( $value ); break; case 'string': $is_valid = is_string( $value ); break; } if ( $is_valid ) { return true; } return new WP_Error( 'rest_invalid_param', /* translators: %s: expected type (bool, array, or string) */ sprintf( __( 'Invalid value type; expected %s', 'woocommerce-payments' ), $expected_type ), [ 'status' => 400 ] ); } /** * Update the option value. * * @param WP_REST_Request $request The request object. * @return WP_Error|WP_REST_Response */ public function update_option( WP_REST_Request $request ) { $option_name = $request->get_param( 'option_name' ); $value = $request->get_param( 'value' ); update_option( $option_name, $value ); return rest_ensure_response( [ 'success' => true, ] ); } } includes/admin/class-wc-rest-payments-authorizations-controller.php 0000777 00000004676 15251156605 0021774 0 ustar 00 <?php /** * Class WC_REST_Payments_Authorizations_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request; use WCPay\Core\Server\Request\List_Authorizations; defined( 'ABSPATH' ) || exit; /** * REST controller for authorizations. */ class WC_REST_Payments_Authorizations_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/authorizations'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_authorizations' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/summary', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_authorizations_summary' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<payment_intent_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_authorization' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve authorizations to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_authorizations( WP_REST_Request $request ) { $wcpay_request = List_Authorizations::from_rest_request( $request ); return $wcpay_request->handle_rest_request(); } /** * Retrieve authorization to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_authorization( WP_REST_Request $request ) { $payment_intent_id = $request->get_param( 'payment_intent_id' ); $request = Request::get( WC_Payments_API_Client::AUTHORIZATIONS_API, $payment_intent_id ); $request->assign_hook( 'wcpay_get_authorization_request' ); return $request->handle_rest_request(); } /** * Retrieve authorizations summary to respond with via API. */ public function get_authorizations_summary() { $request = Request::get( WC_Payments_API_Client::AUTHORIZATIONS_API . '/summary' ); $request->assign_hook( 'wc_pay_get_authorizations_summary' ); return $request->handle_rest_request(); } } includes/admin/class-wc-payments-admin.php 0000777 00000160166 15251156605 0014642 0 ustar 00 <?php /** * Set up top-level menus for WCPay. * * @package WooCommerce\Payments\Admin */ use Automattic\Jetpack\Identity_Crisis as Jetpack_Identity_Crisis; use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Admin\Features\Features; use WCPay\Constants\Intent_Status; use WCPay\Core\Server\Request; use WCPay\Database_Cache; use WCPay\Inline_Script_Payloads\Woo_Payments_Payment_Method_Definitions; use WCPay\Inline_Script_Payloads\Woo_Payments_Payment_Methods_Config; use WCPay\Logger; use WCPay\WooPay\WooPay_Utilities; defined( 'ABSPATH' ) || exit; /** * WC_Payments_Admin Class. */ class WC_Payments_Admin { /** * Badge with number "1" displayed next to a menu item when there is something important to communicate on a page. * * @var string */ const MENU_NOTIFICATION_BADGE = ' <span class="wcpay-menu-badge awaiting-mod count-1"><span class="plugin-count">1</span></span>'; /** * Badge with a count (number of unresolved items) displayed next to a menu item. * Unresolved refers to items that are unread or need action. * * @var string */ const UNRESOLVED_NOTIFICATION_BADGE_FORMAT = ' <span class="wcpay-menu-badge awaiting-mod count-%1$s"><span class="plugin-count">%1$d</span></span>'; /** * WC Payments WordPress Admin menu slug. * * @var string */ const PAYMENTS_SUBMENU_SLUG = 'wc-admin&path=/payments/overview'; /** * Client for making requests to the WooCommerce Payments API. * * @var WC_Payments_API_Client */ protected $payments_api_client; /** * WCPay Gateway instance to get information regarding WooCommerce Payments setup. * * @var WC_Payment_Gateway_WCPay */ private $wcpay_gateway; /** * WC_Payments_Account instance to get information about the account * * @var WC_Payments_Account */ private $account; /** * WC_Payments_Onboarding_Service instance to get information for onboarding. * * @var WC_Payments_Onboarding_Service */ private $onboarding_service; /** * Instance of Order Service for accessing order data. * * @var WC_Payments_Order_Service */ private $order_service; /** * WC_Payments_Incentives_Service instance to get information for incentives. * * @var WC_Payments_Incentives_Service */ private $incentives_service; /** * WC_Payments_PM_Promotions_Service instance to get information for payment method promotions. * * @var WC_Payments_PM_Promotions_Service */ private $pm_promotions_service; /** * WC_Payments_Fraud_Service instance to get information about fraud services. * * @var WC_Payments_Fraud_Service */ private $fraud_service; /** * WCPay admin child pages. * * @var array */ private $admin_child_pages; /** * Database_Cache instance. * * @var Database_Cache */ private $database_cache; /** * The internal cache for WCPay settings passed to JS through script localization. * * This data should only be generated once per request, hence the internal cache. * * @see self::get_js_settings() * * @var ?array */ private $wcpay_js_settings = null; /** * Hook in admin menu items. * * @param WC_Payments_API_Client $payments_api_client WooCommerce Payments API client. * @param WC_Payment_Gateway_WCPay $gateway WCPay Gateway instance to get information regarding WooCommerce Payments setup. * @param WC_Payments_Account $account Account instance. * @param WC_Payments_Onboarding_Service $onboarding_service Onboarding service instance. * @param WC_Payments_Order_Service $order_service Order service instance. * @param WC_Payments_Incentives_Service $incentives_service Incentives service instance. * @param WC_Payments_PM_Promotions_Service $pm_promotions_service PM Promotions service instance. * @param WC_Payments_Fraud_Service $fraud_service Fraud service instance. * @param Database_Cache $database_cache Database Cache instance. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Account $account, WC_Payments_Onboarding_Service $onboarding_service, WC_Payments_Order_Service $order_service, WC_Payments_Incentives_Service $incentives_service, WC_Payments_PM_Promotions_Service $pm_promotions_service, WC_Payments_Fraud_Service $fraud_service, Database_Cache $database_cache ) { $this->payments_api_client = $payments_api_client; $this->wcpay_gateway = $gateway; $this->account = $account; $this->onboarding_service = $onboarding_service; $this->order_service = $order_service; $this->incentives_service = $incentives_service; $this->pm_promotions_service = $pm_promotions_service; $this->fraud_service = $fraud_service; $this->database_cache = $database_cache; } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_action( 'admin_notices', [ $this, 'display_not_supported_currency_notice' ], 9999 ); add_action( 'admin_notices', [ $this, 'display_isk_decimal_notice' ] ); add_action( 'woocommerce_admin_order_data_after_payment_info', [ $this, 'render_order_edit_payment_details_container' ] ); // Add menu items. add_action( 'admin_menu', [ $this, 'add_payments_menu' ], 0 ); // Run this after the redirects in WC_Payments_Account. add_action( 'admin_init', [ $this, 'maybe_redirect_from_payments_admin_child_pages' ], 16 ); add_action( 'admin_enqueue_scripts', [ $this, 'register_payments_scripts' ], 9 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_payments_scripts' ], 9 ); add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'show_woopay_payment_method_name_admin' ] ); add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'display_wcpay_transaction_fee' ] ); add_action( 'admin_init', [ $this, 'redirect_deposits_to_payouts' ] ); add_action( 'woocommerce_update_options_site-visibility', [ $this, 'inform_stripe_when_store_goes_live' ] ); add_action( 'admin_init', [ $this, 'add_css_classes' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_wc_payment_settings_spotlight' ] ); add_action( 'admin_footer', [ $this, 'inject_payment_settings_spotlight_container' ] ); } /** * When a store transitions to live mode, we need to notify Stripe to trigger necessary verification checks. * * @return void */ public function inform_stripe_when_store_goes_live() { $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : ''; // New Settings API uses wp_rest nonce. $nonce_string = Features::is_enabled( 'settings' ) ? 'wp_rest' : 'woocommerce-settings'; if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $nonce_string ) ) { return; } // If an account is not connected, we can skip this. if ( ! $this->account->is_stripe_connected() ) { return; } $coming_soon_value = get_option( 'woocommerce_coming_soon' ); $coming_soon_new_value = sanitize_text_field( wp_unslash( $_POST['woocommerce_coming_soon'] ) ); // If the store is transitioning from coming soon to live, Stripe should be notified. // This is triggered by updating the account business URL. if ( 'no' === $coming_soon_new_value && $coming_soon_value !== $coming_soon_new_value ) { $response = $this->wcpay_gateway->update_account_settings( [ 'account_business_url' => $this->account->get_business_url() ] ); if ( is_wp_error( $response ) ) { Logger::error( 'Failed to update account business URL.' ); } } } /** * Redirect /payments/deposits to /payments/payouts. */ public function redirect_deposits_to_payouts() { if ( is_admin() && isset( $_GET['page'] ) && 'wc-admin' === $_GET['page'] && isset( $_GET['path'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $redirect_map = [ '/payments/deposits' => '/payments/payouts', '/payments/deposits/details' => '/payments/payouts/details', ]; $query_params = $_GET; // phpcs:ignore WordPress.Security.NonceVerification if ( isset( $redirect_map[ $query_params['path'] ] ) ) { $query_params['path'] = $redirect_map[ $query_params['path'] ]; $redirect_url = add_query_arg( $query_params, admin_url( 'admin.php?page=wc-admin' ), ); wp_safe_redirect( $redirect_url ); exit; } } } /** * Add notice explaining that the selected currency is not available. */ public function display_not_supported_currency_notice() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } if ( ! $this->wcpay_gateway->is_available_for_current_currency() ) { ?> <div id="wcpay-unsupported-currency-notice" class="notice notice-warning"> <p> <b> <?php esc_html_e( 'Unsupported currency:', 'woocommerce-payments' ); ?> <?php esc_html( ' ' . get_woocommerce_currency() ); ?> </b> <?php printf( /* translators: %s: WooPayments*/ esc_html__( 'The selected currency is not available for the country set in your %s account.', 'woocommerce-payments' ), 'WooPayments' ); ?> </p> </div> <?php } } /** * Render a container for adding notices to order details screen payment box. */ public function render_order_edit_payment_details_container() { ?> <div id="wcpay-order-payment-details-container"></div> <?php } /** * Add notice explaining that ISK cannot have decimals. */ public function display_isk_decimal_notice() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } if ( get_woocommerce_currency() === 'ISK' && wc_get_price_decimals() !== 0 ) { $url = get_admin_url( null, 'admin.php?page=wc-settings' ); ?> <div id="wcpay-unsupported-currency-notice" class="notice notice-error"> <p> <b> <?php esc_html_e( 'Unsupported currency:', 'woocommerce-payments' ); ?> <?php esc_html( ' ' . get_woocommerce_currency() ); ?> </b> <?php echo wp_kses_post( sprintf( /* Translators: %1$s: Opening anchor tag. %2$s: Closing anchor tag.*/ __( 'Icelandic Króna does not accept decimals. Please update your currency number of decimals to 0 or select a different currency. %1$sVisit settings%2$s', 'woocommerce-payments' ), '<a href="' . $url . '">', '</a>' ) ); ?> </p> </div> <?php } } /** * Add payments menu items. */ public function add_payments_menu() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } global $submenu; $this->admin_child_pages = [ 'wc-payments-overview' => [ 'id' => 'wc-payments-overview', 'title' => __( 'Overview', 'woocommerce-payments' ), 'parent' => 'wc-payments', 'path' => '/payments/overview', 'nav_args' => [ 'parent' => 'wc-payments', 'order' => 10, ], ], 'wc-payments-deposits' => [ 'id' => 'wc-payments-deposits', 'title' => __( 'Payouts', 'woocommerce-payments' ), 'parent' => 'wc-payments', 'path' => '/payments/payouts', 'nav_args' => [ 'parent' => 'wc-payments', 'order' => 20, ], ], 'wc-payments-transactions' => [ 'id' => 'wc-payments-transactions', 'title' => __( 'Transactions', 'woocommerce-payments' ), 'parent' => 'wc-payments', 'path' => '/payments/transactions', 'nav_args' => [ 'parent' => 'wc-payments', 'order' => 30, ], ], 'wc-payments-disputes' => [ 'id' => 'wc-payments-disputes', 'title' => __( 'Disputes', 'woocommerce-payments' ), 'parent' => 'wc-payments', 'path' => '/payments/disputes', 'nav_args' => [ 'parent' => 'wc-payments', 'order' => 40, ], ], ]; try { // Render full payments menu with sub-items only if: // - we have working WPCOM/Jetpack connection; // - the Stripe account is valid (connected, details submitted, and proper capabilities). $should_render_full_menu = $this->account->has_working_jetpack_connection() && $this->account->is_stripe_account_valid(); } catch ( Exception $e ) { // There is an issue with connection, don't render full menu, user will get redirected to the connect page. $should_render_full_menu = false; } $top_level_link = $should_render_full_menu ? '/payments/overview' : '/payments/connect'; $menu_icon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnNjciCiAgIHNvZGlwb2RpOmRvY25hbWU9IndjcGF5X21lbnVfaWNvbi5zdmciCiAgIHdpZHRoPSI4NTIiCiAgIGhlaWdodD0iNjg0IgogICBpbmtzY2FwZTp2ZXJzaW9uPSIxLjEgKGM0ZThmOWUsIDIwMjEtMDUtMjQpIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM3MSIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzY5IgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIGZpdC1tYXJnaW4tdG9wPSIwIgogICAgIGZpdC1tYXJnaW4tbGVmdD0iMCIKICAgICBmaXQtbWFyZ2luLXJpZ2h0PSIwIgogICAgIGZpdC1tYXJnaW4tYm90dG9tPSIwIgogICAgIGlua3NjYXBlOnpvb209IjI1NiIKICAgICBpbmtzY2FwZTpjeD0iLTg0Ljg1NzQyMiIKICAgICBpbmtzY2FwZTpjeT0iLTgzLjI5NDkyMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzMTIiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTA4MSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMTE2IgogICAgIGlua3NjYXBlOndpbmRvdy15PSIyMDIiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmc2NyIgLz4KICA8cGF0aAogICAgIHRyYW5zZm9ybT0ic2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtODUwLCAwKSIKICAgICBkPSJNIDc2OCw4NiBWIDU5OCBIIDg0IFYgODYgWiBtIDAsNTk4IGMgNDgsMCA4NCwtMzggODQsLTg2IFYgODYgQyA4NTIsMzggODE2LDAgNzY4LDAgSCA4NCBDIDM2LDAgMCwzOCAwLDg2IHYgNTEyIGMgMCw0OCAzNiw4NiA4NCw4NiB6IE0gMzg0LDEyOCB2IDQ0IGggLTg2IHYgODQgaCAxNzAgdiA0NCBIIDM0MCBjIC0yNCwwIC00MiwxOCAtNDIsNDIgdiAxMjggYyAwLDI0IDE4LDQyIDQyLDQyIGggNDQgdiA0NCBoIDg0IHYgLTQ0IGggODYgViA0MjggSCAzODQgdiAtNDQgaCAxMjggYyAyNCwwIDQyLC0xOCA0MiwtNDIgViAyMTQgYyAwLC0yNCAtMTgsLTQyIC00MiwtNDIgaCAtNDQgdiAtNDQgeiIKICAgICBmaWxsPSIjYTJhYWIyIgogICAgIGlkPSJwYXRoNjUiIC8+Cjwvc3ZnPgo='; wc_admin_register_page( [ 'id' => 'wc-payments', 'title' => __( 'Payments', 'woocommerce-payments' ), 'capability' => 'manage_woocommerce', 'path' => $top_level_link, 'position' => '55.7', // After WooCommerce & Product menu items. 'icon' => $menu_icon, 'nav_args' => [ 'title' => 'WooPayments', 'is_category' => $should_render_full_menu, 'menuId' => 'plugins', 'is_top_level' => true, ], ] ); // Merchants are unable to see their deposits, transactions, disputes and settings if their account is rejected or under review. // That's expected, because account under review is hard-blocked account that spends in a review pretty short time-frame. // Either merchant gets approved and continues to use payments or they remain suspended and can't use payments. if ( $this->account->is_account_rejected() || $this->account->is_account_under_review() ) { // If the account is rejected, only show the overview page. wc_admin_register_page( $this->admin_child_pages['wc-payments-overview'] ); return; } if ( ! $this->account->is_stripe_connected() ) { wc_admin_register_page( [ 'id' => 'wc-payments-onboarding', 'title' => __( 'Onboarding', 'woocommerce-payments' ), 'parent' => 'wc-payments', 'path' => '/payments/onboarding', 'capability' => 'manage_woocommerce', 'nav_args' => [ 'parent' => 'wc-payments', ], ] ); remove_submenu_page( 'wc-admin&path=/payments/connect', 'wc-admin&path=/payments/onboarding' ); } // We handle how we register this page slightly differently depending on if details are submitted or not. if ( $this->account->is_stripe_connected() && ! $this->account->is_details_submitted() ) { wc_admin_register_page( [ 'id' => 'wc-payments-onboarding-kyc', 'title' => __( 'Continue onboarding', 'woocommerce-payments' ), 'parent' => 'wc-payments', 'path' => '/payments/onboarding/kyc', 'capability' => 'manage_woocommerce', 'nav_args' => [ 'parent' => 'wc-payments', 'order' => 50, ], ] ); remove_submenu_page( 'wc-admin&path=/payments/connect', 'wc-admin&path=/payments/onboarding/kyc' ); } if ( $should_render_full_menu ) { if ( $this->account->is_card_present_eligible() && $this->account->has_card_readers_available() ) { $this->admin_child_pages['wc-payments-card-readers'] = [ 'id' => 'wc-payments-card-readers', 'title' => __( 'Card Readers', 'woocommerce-payments' ), 'parent' => 'wc-payments', 'path' => '/payments/card-readers', 'nav_args' => [ 'parent' => 'wc-payments', 'order' => 50, ], ]; } if ( $this->account->get_capital()['has_previous_loans'] ) { $this->admin_child_pages['wc-payments-capital'] = [ 'id' => 'wc-payments-capital', 'title' => __( 'Capital Loans', 'woocommerce-payments' ), 'parent' => 'wc-payments', 'path' => '/payments/loans', 'nav_args' => [ 'parent' => 'wc-payments', 'order' => 60, ], ]; } if ( WC_Payments_Features::is_documents_section_enabled() ) { $this->admin_child_pages['wc-payments-documents'] = [ 'id' => 'wc-payments-documents', 'title' => __( 'Documents', 'woocommerce-payments' ), 'parent' => 'wc-payments', 'path' => '/payments/documents', 'nav_args' => [ 'parent' => 'wc-payments', 'order' => 50, ], ]; } /** * Please note that if any other page is registered first and it's * path is different from the $top_level_link it will make * wc_admin_register_page to duplicate "Payments" menu item as a * first item in the sub-menu. */ foreach ( $this->admin_child_pages as $admin_child_page ) { wc_admin_register_page( $admin_child_page ); } // Remove the "Continue onboarding" submenu item, if it exists. if ( in_array( 'wc-payments-onboarding-kyc', array_keys( $this->admin_child_pages ), true ) ) { remove_submenu_page( 'wc-admin&path=/payments/overview', 'wc-admin&path=/payments/onboarding/kyc' ); } wc_admin_connect_page( [ 'id' => 'woocommerce-settings-payments-woocommerce-payments', 'parent' => 'woocommerce-settings-payments', 'screen_id' => 'woocommerce_page_wc-settings-checkout-woocommerce_payments', 'title' => 'WooPayments', 'nav_args' => [ 'parent' => 'wc-payments', 'title' => __( 'Settings', 'woocommerce-payments' ), 'url' => 'wc-settings&tab=checkout§ion=woocommerce_payments', 'order' => 99, ], ] ); // Add the Settings submenu directly to the array, it's the only way to make it link to an absolute URL. $submenu_keys = array_keys( $submenu ); $last_submenu_key = $submenu_keys[ count( $submenu ) - 1 ]; $submenu[ $last_submenu_key ][] = [ // PHPCS:Ignore WordPress.WP.GlobalVariablesOverride.Prohibited $this->get_settings_menu_item_name(), 'manage_woocommerce', WC_Payments_Admin_Settings::get_settings_url(), ]; // Temporary fix to settings menu disappearance is to register the page after settings menu has been manually added. // TODO: More robust solution is to be implemented by https://github.com/Automattic/woocommerce-payments/issues/231. wc_admin_register_page( [ 'id' => 'wc-payments-deposit-details', 'title' => __( 'Payout details', 'woocommerce-payments' ), 'parent' => 'wc-payments-transactions', // Not (top level) deposits, as workaround for showing up as submenu page. 'path' => '/payments/payouts/details', ] ); wc_admin_register_page( [ 'id' => 'wc-payments-transaction-details', 'title' => __( 'Payment details', 'woocommerce-payments' ), 'parent' => 'wc-payments-transactions', 'path' => '/payments/transactions/details', ] ); wc_admin_register_page( [ 'id' => 'wc-payments-disputes-details-legacy-redirect', 'title' => __( 'Dispute details', 'woocommerce-payments' ), 'parent' => 'wc-payments-disputes', 'path' => '/payments/disputes/details', ] ); wc_admin_register_page( [ 'id' => 'wc-payments-disputes-challenge', 'title' => __( 'Challenge dispute', 'woocommerce-payments' ), 'parent' => 'wc-payments-disputes-details', 'path' => '/payments/disputes/challenge', ] ); wc_admin_register_page( [ 'id' => 'wc-payments-additional-payment-methods', 'parent' => 'woocommerce-settings-payments', 'title' => __( 'Add new payment methods', 'woocommerce-payments' ), 'path' => '/payments/additional-payment-methods', ] ); wc_admin_register_page( [ 'id' => 'wc-payments-multi-currency-setup', 'parent' => 'woocommerce-settings-payments', 'title' => __( 'Set up multiple currencies', 'woocommerce-payments' ), 'path' => '/payments/multi-currency-setup', ] ); } WC_Payments_Utils::enqueue_style( 'wcpay-admin-css', plugins_url( 'assets/css/admin.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'assets/css/admin.css' ), 'all' ); $this->add_menu_notification_badge(); $this->add_disputes_notification_badge(); if ( $this->wcpay_gateway->get_option( 'manual_capture' ) === 'yes' ) { $this->add_transactions_notification_badge(); } } /** * Register the CSS and JS scripts */ public function register_payments_scripts() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } WC_Payments::register_script_with_dependencies( 'WCPAY_DASH_APP', 'dist/index', [ 'wp-api-request' ] ); wp_add_inline_script( 'WCPAY_DASH_APP', new Woo_Payments_Payment_Method_Definitions(), 'before' ); wp_add_inline_script( 'WCPAY_DASH_APP', new Woo_Payments_Payment_Methods_Config(), 'before' ); wp_set_script_translations( 'WCPAY_DASH_APP', 'woocommerce-payments' ); WC_Payments_Utils::register_style( 'WCPAY_DASH_APP', plugins_url( 'dist/index.css', WCPAY_PLUGIN_FILE ), [ 'wc-components' ], WC_Payments::get_file_version( 'dist/index.css' ), 'all' ); WC_Payments::register_script_with_dependencies( 'WCPAY_TOS', 'dist/tos', [ 'wp-components' ] ); wp_set_script_translations( 'WCPAY_TOS', 'woocommerce-payments' ); WC_Payments_Utils::register_style( 'WCPAY_TOS', plugins_url( 'dist/tos.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'dist/tos.css' ), 'all' ); WC_Payments::register_script_with_dependencies( 'WCPAY_ADMIN_ORDER_ACTIONS', 'dist/order', [ 'jquery-tiptip', 'wp-components' ] ); WC_Payments_Utils::register_style( 'WCPAY_ADMIN_ORDER_ACTIONS', plugins_url( 'dist/order.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'dist/order.css' ), 'all' ); WC_Payments::register_script_with_dependencies( 'WCPAY_ADMIN_SETTINGS', 'dist/settings' ); wp_add_inline_script( 'WCPAY_ADMIN_SETTINGS', new Woo_Payments_Payment_Method_Definitions(), 'before' ); wp_localize_script( 'WCPAY_ADMIN_SETTINGS', 'wcpayExpressCheckoutParams', [ 'stripe' => [ 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ), 'accountId' => $this->account->get_stripe_account_id(), 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), ], ] ); wp_set_script_translations( 'WCPAY_ADMIN_SETTINGS', 'woocommerce-payments' ); WC_Payments_Utils::register_style( 'WCPAY_ADMIN_SETTINGS', plugins_url( 'dist/settings.css', WCPAY_PLUGIN_FILE ), [ 'wc-components' ], WC_Payments::get_file_version( 'dist/settings.css' ), 'all' ); WC_Payments::register_script_with_dependencies( 'WCPAY_PLUGINS_PAGE', 'dist/plugins-page', [ 'wp-api-request', 'wp-components' ] ); wp_set_script_translations( 'WCPAY_PLUGINS_PAGE', 'woocommerce-payments' ); WC_Payments_Utils::register_style( 'WCPAY_PLUGINS_PAGE', plugins_url( 'dist/plugins-page.css', WCPAY_PLUGIN_FILE ), [ 'wp-components', 'wc-components' ], WC_Payments::get_file_version( 'dist/plugins-page.css' ), 'all' ); WC_Payments::register_script_with_dependencies( 'WCPAY_WC_PAYMENTS_SETTINGS_SPOTLIGHT', 'dist/wc-payments-settings-spotlight' ); wp_set_script_translations( 'WCPAY_WC_PAYMENTS_SETTINGS_SPOTLIGHT', 'woocommerce-payments' ); WC_Payments_Utils::register_style( 'WCPAY_WC_PAYMENTS_SETTINGS_SPOTLIGHT', plugins_url( 'dist/wc-payments-settings-spotlight.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'dist/wc-payments-settings-spotlight.css' ), 'all' ); } /** * Load the assets */ public function enqueue_payments_scripts() { global $current_tab; // Enqueue the admin settings assets on any WCPay settings page. // We also need to enqueue and localize on the multi-currency tab. if ( WC_Payments_Utils::is_payments_settings_page() || 'wcpay_multi_currency' === $current_tab ) { // Localize before actually enqueuing to avoid unnecessary settings generation. // Most importantly, the destructive error transient handling. wp_localize_script( 'WCPAY_ADMIN_SETTINGS', 'wcpaySettings', $this->get_js_settings() ); // Output the settings JS and CSS only on the settings page. wp_enqueue_script( 'WCPAY_ADMIN_SETTINGS' ); wp_enqueue_style( 'WCPAY_ADMIN_SETTINGS' ); } // Enqueue the onboarding scripts if the user is on the onboarding page. if ( WC_Payments_Utils::is_onboarding_page() ) { wp_localize_script( 'WCPAY_ONBOARDING_SETTINGS', 'wcpayOnboardingSettings', [] ); } // TODO: Try to enqueue the JS and CSS bundles lazily (will require changes on WC-Admin). $current_screen = get_current_screen() ? get_current_screen()->base : null; if ( wc_admin_is_registered_page() || 'widgets' === $current_screen ) { // Localize before actually enqueuing to avoid unnecessary settings generation. // Most importantly, the destructive error transient handling. wp_localize_script( 'WCPAY_DASH_APP', 'wcpaySettings', $this->get_js_settings() ); wp_enqueue_script( 'WCPAY_DASH_APP' ); wp_enqueue_style( 'WCPAY_DASH_APP' ); } // TODO: Update conditions when ToS script is enqueued. $tos_agreement_declined = ( 'checkout' === $current_tab && isset( $_GET['tos-disabled'] ) // phpcs:ignore WordPress.Security.NonceVerification ); $tos_agreement_required = ( $this->is_tos_agreement_required() && ( WC_Payments_Utils::is_payments_settings_page() || // Or a WC Admin page? // Note: Merchants can navigate from analytics to payments w/o reload, // which is why this is necessary. wc_admin_is_registered_page() ) ); $track_stripe_connected = get_option( '_wcpay_onboarding_stripe_connected' ); if ( $tos_agreement_declined || $tos_agreement_required || $track_stripe_connected ) { // phpcs:ignore WordPress.Security.NonceVerification wp_localize_script( 'WCPAY_TOS', 'wcpay_tos_settings', [ 'settingsUrl' => WC_Payments_Admin_Settings::get_settings_url(), 'tosAgreementRequired' => $tos_agreement_required, 'tosAgreementDeclined' => $tos_agreement_declined, 'trackStripeConnected' => $track_stripe_connected, ] ); wp_enqueue_script( 'WCPAY_TOS' ); wp_enqueue_style( 'WCPAY_TOS' ); } $screen = get_current_screen(); // Only enqueue the scripts on the plugins page. if ( in_array( $screen->id, [ 'plugins' ], true ) ) { // Localize before actually enqueuing to avoid unnecessary settings generation. // Most importantly, the destructive error transient handling. wp_localize_script( 'WCPAY_PLUGINS_PAGE', 'wcpayPluginsSettings', $this->get_plugins_page_js_settings() ); wp_enqueue_script( 'WCPAY_PLUGINS_PAGE' ); wp_enqueue_style( 'WCPAY_PLUGINS_PAGE' ); add_action( 'admin_footer', [ $this, 'load_plugins_page_wrapper' ] ); } if ( in_array( $screen->id, [ 'shop_order', 'woocommerce_page_wc-orders' ], true ) ) { $order = wc_get_order(); if ( $order && strpos( $order->get_payment_method(), WC_Payment_Gateway_WCPay::GATEWAY_ID ) !== false ) { $refund_amount = $order->get_remaining_refund_amount(); // Check if the order's test mode meta matches the site's current test mode state. // E.g. order and site are both in test mode, or both in live mode. $order_mode = $order->get_meta( WC_Payments_Order_Service::WCPAY_MODE_META_KEY ); if ( '' === $order_mode ) { // If the order doesn't have a mode set, assume it was created before the order mode meta was added (< 6.9 PR#7651) and return null. $order_test_mode_match = null; } else { $order_test_mode_match = ( \WCPay\Constants\Order_Mode::PRODUCTION === $order_mode && WC_Payments::mode()->is_live() ) || ( \WCPay\Constants\Order_Mode::TEST === $order_mode && WC_Payments::mode()->is_test() ); } wp_localize_script( 'WCPAY_ADMIN_ORDER_ACTIONS', 'wcpay_order_config', [ 'disableManualRefunds' => ! $this->wcpay_gateway->has_refund_failed( $order ), 'manualRefundsTip' => __( 'Refunding manually requires reimbursing your customer offline via cash, check, etc. The refund amounts entered here will only be used to balance your analytics.', 'woocommerce-payments' ), 'refundAmount' => $refund_amount, 'formattedRefundAmount' => wp_strip_all_tags( wc_price( $refund_amount, [ 'currency' => $order->get_currency() ] ) ), 'refundedAmount' => $order->get_total_refunded(), 'canRefund' => $this->wcpay_gateway->can_refund_order( $order ), 'chargeId' => $this->order_service->get_charge_id_for_order( $order ), 'hasOpenAuthorization' => $this->order_service->has_open_authorization( $order ), 'testMode' => \WCPay\Constants\Order_Mode::TEST === $order->get_meta( WC_Payments_Order_Service::WCPAY_MODE_META_KEY ), 'orderTestModeMatch' => $order_test_mode_match, ] ); wp_localize_script( 'WCPAY_ADMIN_ORDER_ACTIONS', 'wcpaySettings', $this->get_js_settings() ); wp_enqueue_script( 'WCPAY_ADMIN_ORDER_ACTIONS' ); WC_Payments_Utils::enqueue_style( 'WCPAY_ADMIN_ORDER_ACTIONS' ); } } } /** * Outputs the wrapper for the plugin modal * Contents are loaded by React script * * @return void */ public function load_plugins_page_wrapper() { wc_get_template( 'plugins-page/plugins-page-wrapper.php', [], '', WCPAY_ABSPATH . 'templates/' ); } /** * Get the WCPay settings to be sent to JS. * * It used an internal cache to make sure it only generates the settings once per request. * This is needed in order to avoid performance issues and simplify error transients handling. * * @return array */ private function get_js_settings(): array { // Return the internally cached data if it is already initialized. if ( ! is_null( $this->wcpay_js_settings ) ) { return $this->wcpay_js_settings; } $error_message = get_transient( WC_Payments_Account::ERROR_MESSAGE_TRANSIENT ); delete_transient( WC_Payments_Account::ERROR_MESSAGE_TRANSIENT ); /** * This is a workaround to pass the current user's email address to WCPay's settings until we do not need to rely * on backwards compatibility and can use `getCurrentUser` from `@wordpress/core-data`. */ $current_user = wp_get_current_user(); $current_user_email = $current_user && $current_user->user_email ? $current_user->user_email : get_option( 'admin_email' ); if ( version_compare( WC_VERSION, '6.0', '<' ) ) { $path = WCPAY_ABSPATH . 'i18n/locale-info.php'; } else { $path = WC()->plugin_path() . '/i18n/locale-info.php'; } $locale_info = include $path; // Get symbols for those currencies without a short one. $symbols = get_woocommerce_currency_symbols(); $currency_data = []; foreach ( $locale_info as $key => $value ) { $currency_code = $value['currency_code'] ?? ''; $default_locale_formatting = $value['locales']['default'] ?? []; $currency_data[ $key ] = [ 'code' => $currency_code, 'symbol' => $value['short_symbol'] ?? $symbols[ $currency_code ] ?? '', 'symbolPosition' => $value['currency_pos'] ?? '', 'thousandSeparator' => $value['thousand_sep'] ?? '', 'decimalSeparator' => $value['decimal_sep'] ?? '', 'precision' => $value['num_decimals'], 'defaultLocale' => [ 'symbolPosition' => $default_locale_formatting['currency_pos'] ?? '', 'thousandSeparator' => $default_locale_formatting['thousand_sep'] ?? '', 'decimalSeparator' => $default_locale_formatting['decimal_sep'] ?? '', ], ]; } $account_status_data = $this->account->get_account_status_data(); $account_is_valid = $this->account->is_stripe_account_valid(); $test_mode = false; try { $test_mode = WC_Payments::mode()->is_test(); } catch ( Exception $e ) { Logger::log( sprintf( 'WooPayments JS settings: Could not determine if the gateway should process payments in test mode! Message: %s', $e->getMessage() ), 'warning' ); } $test_mode_onboarding = false; try { $test_mode_onboarding = WC_Payments::mode()->is_test_mode_onboarding(); } catch ( Exception $e ) { Logger::log( sprintf( 'WooPayments JS settings: Could not determine if we should onboard accounts in test mode! Message: %s', $e->getMessage() ), 'warning' ); } $dev_mode = false; try { $dev_mode = WC_Payments::mode()->is_dev(); } catch ( Exception $e ) { Logger::log( sprintf( 'WooPayments JS settings: Could not determine if the gateway should be in dev mode! Message: %s', $e->getMessage() ), 'warning' ); } $connect_url = WC_Payments_Account::get_connect_url(); $connect_incentive = $this->incentives_service->get_connect_incentive(); // If we have an incentive ID, attach it to the connect URL. if ( ! empty( $connect_incentive['id'] ) ) { $connect_url = add_query_arg( [ 'promo' => sanitize_text_field( $connect_incentive['id'] ) ], $connect_url ); } // Get the site logo URL, if available. $site_logo_id = get_theme_mod( 'custom_logo' ); $site_logo_url = $site_logo_id ? ( wp_get_attachment_image_src( $site_logo_id, 'full' )[0] ?? '' ) : ''; $this->wcpay_js_settings = [ 'version' => WCPAY_VERSION_NUMBER, 'connectUrl' => $connect_url, 'overviewUrl' => WC_Payments_Account::get_overview_page_url(), 'connect' => [ 'country' => WC()->countries->get_base_country(), 'availableCountries' => WC_Payments_Utils::supported_countries(), 'availableStates' => WC()->countries->get_states(), ], 'connectIncentive' => $connect_incentive, 'devMode' => $dev_mode, 'testModeOnboarding' => $test_mode_onboarding, 'testMode' => $test_mode, // Set this flag for use in the front-end to alter messages and notices if on-boarding has been disabled. 'onBoardingDisabled' => WC_Payments_Account::is_on_boarding_disabled(), 'onboardingFieldsData' => $account_is_valid ? null : $this->onboarding_service->get_fields_data( get_user_locale() ), 'onboardingEmbeddedKycInProgress' => $this->onboarding_service->is_embedded_kyc_in_progress(), 'errorMessage' => $error_message, 'featureFlags' => $this->get_frontend_feature_flags(), 'isSubscriptionsActive' => class_exists( 'WC_Subscriptions' ) && version_compare( WC_Subscriptions::$version, '2.2.0', '>=' ), // Used in the settings page by the AccountFees component. 'zeroDecimalCurrencies' => WC_Payments_Utils::zero_decimal_currencies(), 'fraudServices' => $this->fraud_service->get_fraud_services_config(), 'isJetpackConnected' => $this->account->has_working_jetpack_connection(), 'isJetpackIdcActive' => Jetpack_Identity_Crisis::has_identity_crisis(), 'isAccountConnected' => $this->account->has_account_data(), 'isAccountValid' => $account_is_valid, 'accountStatus' => $account_status_data, 'accountFees' => $this->account->get_fees(), 'accountLoans' => $this->account->get_capital(), 'accountEmail' => $this->account->get_account_email(), 'accountDetails' => $this->account->get_account_details(), 'showUpdateDetailsTask' => $this->get_should_show_update_business_details_task( $account_status_data ), 'wpcomReconnectUrl' => $this->payments_api_client->is_server_connected() && ! $this->payments_api_client->has_server_connection_owner() ? WC_Payments_Account::get_wpcom_reconnect_url() : null, 'multiCurrencySetup' => [ 'isSetupCompleted' => filter_var( get_option( 'wcpay_multi_currency_setup_completed' ), FILTER_VALIDATE_BOOLEAN ) ? 'yes' : 'no', ], 'isMultiCurrencyEnabled' => WC_Payments_Features::is_customer_multi_currency_enabled(), 'shouldUseExplicitPrice' => WC_Payments_Explicit_Price_Formatter::should_output_explicit_price(), 'overviewTasksVisibility' => [ 'dismissedTodoTasks' => get_option( 'woocommerce_dismissed_todo_tasks', [] ), 'deletedTodoTasks' => get_option( 'woocommerce_deleted_todo_tasks', [] ), 'remindMeLaterTodoTasks' => get_option( 'woocommerce_remind_me_later_todo_tasks', [] ), ], 'currentUserEmail' => $current_user_email, 'currencyData' => $currency_data, 'restUrl' => get_rest_url( null, '' ), // rest url to concatenate when merchant use Plain permalinks. 'siteLogoUrl' => $site_logo_url, 'fraudProtection' => [ 'isWelcomeTourDismissed' => WC_Payments_Features::is_fraud_protection_welcome_tour_dismissed(), ], 'accountDefaultCurrency' => $this->account->get_account_default_currency(), 'storeCurrency' => get_option( 'woocommerce_currency' ), 'isWooPayStoreCountryAvailable' => WooPay_Utilities::is_store_country_available(), 'woopayLastDisableDate' => $this->wcpay_gateway->get_option( 'platform_checkout_last_disable_date' ), 'isStripeBillingEnabled' => WC_Payments_Features::is_stripe_billing_enabled(), 'isStripeBillingEligible' => WC_Payments_Features::is_stripe_billing_eligible(), 'storeName' => get_bloginfo( 'name' ), 'isNextDepositNoticeDismissed' => WC_Payments_Features::is_next_deposit_notice_dismissed(), 'isInstantDepositNoticeDismissed' => get_option( 'wcpay_instant_deposit_notice_dismissed', false ), 'instantDepositsPreviouslyEligible' => get_option( 'wcpay_instant_deposits_previously_eligible', false ), 'dismissedDuplicateNotices' => get_option( 'wcpay_duplicate_payment_method_notices_dismissed', [] ), 'isConnectionSuccessModalDismissed' => get_option( WC_Payments_Onboarding_Service::ONBOARDING_CONNECTION_SUCCESS_MODAL_OPTION, false ), 'isOverviewSurveySubmitted' => get_option( 'wcpay_survey_payment_overview_submitted', false ), 'trackingInfo' => $this->account->get_tracking_info(), 'lifetimeTPV' => $this->account->get_lifetime_total_payment_volume(), 'defaultExpressCheckoutBorderRadius' => WC_Payments_Express_Checkout_Button_Handler::DEFAULT_BORDER_RADIUS_IN_PX, 'isWooPayGlobalThemeSupportEligible' => WC_Payments_Features::is_woopay_global_theme_support_eligible(), 'dateFormat' => wc_date_format(), 'timeFormat' => get_option( 'time_format' ), ]; /** * Filter the WCPay JS settings. * * @since 6.1.0 */ return apply_filters( 'wcpay_js_settings', $this->wcpay_js_settings ); } /** * Get the WCPay plugins page settings to be sent to JS. * * @return array */ private function get_plugins_page_js_settings(): array { $plugins_page_settings = [ 'exitSurveyLastShown' => get_option( 'wcpay_exit_survey_last_shown', null ), ]; /** * Filter the plugins page settings. * * @since 7.8.0 */ return apply_filters( 'wcpay_plugins_page_js_settings', $plugins_page_settings ); } /** * Creates an array of features enabled only when external dependencies are of certain versions. * * @return array An associative array containing the flags as booleans. */ private function get_frontend_feature_flags(): array { return array_merge( [ 'paymentTimeline' => self::version_compare( WC_ADMIN_VERSION_NUMBER, '1.4.0', '>=' ), 'customSearch' => self::version_compare( WC_ADMIN_VERSION_NUMBER, '1.3.0', '>=' ), ], WC_Payments_Features::to_array() ); } /** * A wrapper around version_compare to allow comparing two version numbers even when they are suffixed with a dash and a string, for example 1.3.0-beta. * * @param string $version1 First version number. * @param string $version2 Second version number. * @param string $operator A boolean operator to use when comparing. * * @return bool True if the relationship is the one specified by the operator. */ private static function version_compare( $version1, $version2, string $operator ): bool { // Attempt to extract version numbers. $version_regex = '/^([\d\.]+)(-.*)?$/'; if ( preg_match( $version_regex, $version1, $matches1 ) && preg_match( $version_regex, $version2, $matches2 ) ) { // Only compare the numeric parts of the versions, ignore the bit after the dash. $version1 = $matches1[1]; $version2 = $matches2[1]; } return (bool) version_compare( $version1, $version2, $operator ); } /** * Checks whether it's necessary to display a ToS agreement modal. * * @return bool */ private function is_tos_agreement_required() { // The gateway might already be disabled because of ToS. if ( ! $this->wcpay_gateway->is_enabled() ) { return false; } // Retrieve the latest agreement and check whether it's regarding the latest ToS version. $agreement = $this->account->get_latest_tos_agreement(); if ( empty( $agreement ) ) { // Account data couldn't be fetched, let the merchant solve that first. return false; } return ! $agreement['is_current_version']; } /** * Attempts to add a notification badge on WordPress menu next to Payments menu item * to remind user that setup is required. */ public function add_menu_notification_badge() { global $menu; // If plugin activation date is less than 3 days, do not show the badge. $past_3_days = time() - get_option( 'wcpay_activation_timestamp', 0 ) >= ( 3 * DAY_IN_SECONDS ); if ( false === $past_3_days ) { return; } // First, lets see what the DB option says. $hide_badge = 'yes' === get_option( 'wcpay_menu_badge_hidden', 'no' ); // There are situations where we need to force show the badge. // If we have: // - a broken Jetpack connection and a connected account; // - or working Jetpack connection and a connected but invalid account. // show the badge since the merchant needs to take action. if ( ( ! $this->account->has_working_jetpack_connection() && $this->account->has_account_data() ) || ( $this->account->has_working_jetpack_connection() && $this->account->has_account_data() && ! $this->account->is_stripe_account_valid() ) ) { $hide_badge = false; } elseif ( $this->account->has_working_jetpack_connection() && $this->account->is_stripe_account_valid() ) { // If everything is working fine, hide the badge regardless of the DB option. $hide_badge = true; } if ( $hide_badge ) { return; } $badge = self::MENU_NOTIFICATION_BADGE; foreach ( $menu as $index => $menu_item ) { if ( false === strpos( $menu_item[0], $badge ) && ( 'wc-admin&path=/payments/connect' === $menu_item[2] ) ) { $menu[ $index ][0] .= $badge; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // One menu item with a badge is more than enough. break; } } } /** * Check whether a setup task needs to be displayed prompting the user to update * their business details. * * @param array $account_status_data An array containing the account status data. * * @return bool True if we should show the task, false otherwise. */ public function get_should_show_update_business_details_task( array $account_status_data ) { $status = $account_status_data['status'] ?? ''; $current_deadline = $account_status_data['currentDeadline'] ?? false; $past_due = $account_status_data['pastDue'] ?? false; // If the account is restricted_soon, but there's no current deadline, no action is needed. if ( ( 'restricted_soon' === $status && $current_deadline ) || ( 'restricted' === $status && $past_due ) ) { return true; } return false; } /** * Returns the name to display for the "Payments > Settings" submenu item. * * The name will also contain a notification badge if the UPE settings preview is enabled but UPE is not. * * @return string */ private function get_settings_menu_item_name() { return __( 'Settings', 'woocommerce' ); // PHPCS:Ignore WordPress.WP.I18n.TextDomainMismatch } /** * Redirects WCPay admin pages to the Connect page for stores that * don't have a working Jetpack connection or a valid connected Stripe account. * * Please note that the overview page is handled separately in the * `WC_Payments_Account::maybe_redirect_from_overview_page` method, before this method is called (priority 15 vs 16). * * IMPORTANT: The logic should be kept in sync with the one in maybe_redirect_from_connect_page to avoid loops. * * @see WC_Payments_Account::maybe_redirect_from_overview_page() for overview page handling. * @see WC_Payments_Account::maybe_handle_onboarding() for connect links handling. * * @return bool True if a redirection happened, false otherwise. */ public function maybe_redirect_from_payments_admin_child_pages(): bool { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return false; } $url_params = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification if ( empty( $url_params['page'] ) || 'wc-admin' !== $url_params['page'] ) { return false; } $current_path = ! empty( $url_params['path'] ) ? $url_params['path'] : ''; if ( empty( $current_path ) ) { return false; } // If the current path doesn't match any of the paths we're interested in, do not redirect. $page_paths = []; foreach ( $this->admin_child_pages as $payments_child_page ) { $page_paths[] = preg_quote( $payments_child_page['path'], '/' ); } if ( ! preg_match( '/^(' . implode( '|', $page_paths ) . ')/', $current_path ) ) { return false; } // If everything is NOT in good working condition, redirect to Payments Connect page. if ( ! $this->account->has_working_jetpack_connection() || ! $this->account->is_stripe_account_valid() ) { $this->account->redirect_to_onboarding_welcome_page( sprintf( /* translators: 1: WooPayments. */ __( 'Please <b>complete your %1$s setup</b> to continue using it.', 'woocommerce-payments' ), 'WooPayments' ) ); return true; } return false; } /** * Add woopay as a payment method to the edit order on admin. * * @param int $order_id order_id. */ public function show_woopay_payment_method_name_admin( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order || ! $order->get_meta( 'is_woopay' ) ) { return; } ?> <div class="wc-payment-gateway-method-name-woopay-wrapper"> <?php echo esc_html_e( 'Paid with', 'woocommerce-payments' ) . ' '; ?> <img alt="WooPay" src="<?php echo esc_url_raw( plugins_url( 'assets/images/payment-methods/woo-short.svg', WCPAY_PLUGIN_FILE ) ); ?>"> <?php if ( $order->get_meta( 'last4' ) ) { echo esc_html_e( 'Card ending in', 'woocommerce-payments' ) . ' '; echo esc_html( $order->get_meta( 'last4' ) ); } ?> </div> <?php } /** * Display the _wcpay_transaction_fee from order metadata to the Order Edit screen on admin. * * @param int $order_id order_id. */ public function display_wcpay_transaction_fee( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order || ! $order->get_meta( '_wcpay_transaction_fee' ) || Intent_Status::REQUIRES_CAPTURE === $order->get_meta( WC_Payments_Order_Service::INTENTION_STATUS_META_KEY ) ) { return; } ?> <tr> <td class="label wcpay-transaction-fee"> <?php // phpcs:ignore WordPress.Security.EscapeOutput echo wc_help_tip( sprintf( /* translators: %s: WooPayments */ __( 'This represents the fee %s collects for the transaction.', 'woocommerce-payments' ), 'WooPayments' ) ); ?> <?php esc_html_e( 'Transaction Fee:', 'woocommerce-payments' ); ?> </td> <td width="1%"></td> <td class="total"> -<?php echo wp_kses( wc_price( $order->get_meta( '_wcpay_transaction_fee' ), [ 'currency' => $order->get_currency() ] ), 'post' ); ?> </td> </tr> <?php } /** * Adds a notification badge to the Payments > Disputes admin menu item to * indicate the number of disputes that need a response. */ public function add_disputes_notification_badge() { global $submenu; if ( ! isset( $submenu[ self::PAYMENTS_SUBMENU_SLUG ] ) ) { return; } $disputes_needing_response = $this->get_disputes_awaiting_response_count(); if ( $disputes_needing_response <= 0 ) { return; } foreach ( $submenu[ self::PAYMENTS_SUBMENU_SLUG ] as $index => $menu_item ) { if ( 'wc-admin&path=/payments/disputes' === $menu_item[2] ) { // Direct the user to the disputes which need a response by default. $submenu[ self::PAYMENTS_SUBMENU_SLUG ][ $index ][2] = admin_url( add_query_arg( [ 'filter' => 'awaiting_response' ], 'admin.php?page=' . $menu_item[2] ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // Append the dispute notification badge to indicate the number of disputes needing a response. $submenu[ self::PAYMENTS_SUBMENU_SLUG ][ $index ][0] .= sprintf( self::UNRESOLVED_NOTIFICATION_BADGE_FORMAT, esc_html( $disputes_needing_response ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited break; } } } /** * Adds a notification badge to the Payments > Transactions admin menu item to * indicate the number of transactions that need to be captured. * * @return void */ public function add_transactions_notification_badge() { global $submenu; if ( ! isset( $submenu[ self::PAYMENTS_SUBMENU_SLUG ] ) ) { return; } $uncaptured_transactions = $this->get_uncaptured_transactions_count(); if ( $uncaptured_transactions <= 0 ) { return; } foreach ( $submenu[ self::PAYMENTS_SUBMENU_SLUG ] as $index => $menu_item ) { if ( 'wc-admin&path=/payments/transactions' === $menu_item[2] ) { $submenu[ self::PAYMENTS_SUBMENU_SLUG ][ $index ][0] .= sprintf( self::UNRESOLVED_NOTIFICATION_BADGE_FORMAT, esc_html( $uncaptured_transactions ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited break; } } } /** * Add new custom css classes. * * @return void */ public function add_css_classes() { global $current_tab; if ( 'checkout' === $current_tab ) { add_filter( 'admin_body_class', static function ( $classes ) { return "$classes woocommerce-payments-checkout-section"; } ); } } /** * Gets the number of disputes which need a response. ie have a 'needs_response' or 'warning_needs_response' status. * Used to display a notification badge on the Payments > Disputes menu item. * * @return int The number of disputes which need a response. */ private function get_disputes_awaiting_response_count() { $test_mode = WC_Payments::mode()->is_test(); $cache_key = $test_mode ? Database_Cache::DISPUTE_STATUS_COUNTS_KEY_TEST_MODE : Database_Cache::DISPUTE_STATUS_COUNTS_KEY; $send_callback = function () { $request = Request::get( WC_Payments_API_Client::DISPUTES_API . '/status_counts' ); $request->assign_hook( 'wcpay_get_dispute_status_counts' ); return $request->send(); }; $disputes_status_counts = $this->database_cache->get_or_add( $cache_key, $send_callback, // We'll consider all array values to be valid as the cache is only invalidated when it is deleted or it expires. 'is_array' ); if ( empty( $disputes_status_counts ) ) { return 0; } $needs_response_statuses = [ 'needs_response', 'warning_needs_response' ]; return (int) array_sum( array_intersect_key( $disputes_status_counts, array_flip( $needs_response_statuses ) ) ); } /** * Gets the number of uncaptured transactions, that is authorizations that need to be captured within 7 days. * * @return int The number of uncaptured transactions. */ private function get_uncaptured_transactions_count() { $test_mode = WC_Payments::mode()->is_test(); $cache_key = $test_mode ? DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY_TEST_MODE : DATABASE_CACHE::AUTHORIZATION_SUMMARY_KEY; $send_callback = function () { $request = Request::get( WC_Payments_API_Client::AUTHORIZATIONS_API . '/summary' ); $request->assign_hook( 'wc_pay_get_authorizations_summary' ); return $request->send(); }; $authorization_summary = $this->database_cache->get_or_add( $cache_key, $send_callback, // We'll consider all array values to be valid as the cache is only invalidated when it is deleted or it expires. 'is_array' ); if ( empty( $authorization_summary ) ) { return 0; } return $authorization_summary['count']; } /** * Enqueue the spotlight promotion script on WooCommerce Payments Settings page. * Only runs on WooCommerce 9.9.2+ (when the new WooCommerce Payments Settings UI was enabled for all stores). */ public function enqueue_wc_payment_settings_spotlight() { // Check for minimum WooCommerce version 9.9.2. if ( ! Constants::is_defined( 'WC_VERSION' ) || version_compare( Constants::get_constant( 'WC_VERSION' ), '9.9.2', '<' ) ) { return; } // Only enqueue on the WooCommerce Payments Settings page. if ( ! $this->is_wc_admin_payments_settings_page() ) { return; } // Only enqueue if there are visible spotlight promotions. $promotions = $this->pm_promotions_service->get_visible_promotions(); if ( empty( $promotions ) ) { return; } $has_spotlight = false; foreach ( $promotions as $promotion ) { if ( isset( $promotion['type'] ) && 'spotlight' === $promotion['type'] ) { $has_spotlight = true; break; } } if ( ! $has_spotlight ) { return; } wp_enqueue_script( 'WCPAY_WC_PAYMENTS_SETTINGS_SPOTLIGHT' ); wp_enqueue_style( 'WCPAY_WC_PAYMENTS_SETTINGS_SPOTLIGHT' ); } /** * Inject the container div for the spotlight promotion on WooCommerce payment settings page. * Only runs on WooCommerce 9.9.2+ (when the new WooCommerce Payments Settings UI was enabled for all stores). */ public function inject_payment_settings_spotlight_container() { // Check for minimum WooCommerce version 9.9.2. if ( ! Constants::is_defined( 'WC_VERSION' ) || version_compare( Constants::get_constant( 'WC_VERSION' ), '9.9.2', '<' ) ) { return; } // Only inject on the WooCommerce Payments settings page. if ( ! $this->is_wc_admin_payments_settings_page() ) { return; } echo '<div id="wcpay-payments-settings-spotlight"></div>'; } /** * Check if we're on the WooCommerce Payments Settings page. * * @return bool True if on the WC payment settings page. */ private function is_wc_admin_payments_settings_page(): bool { // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return isset( $_REQUEST['page'] ) && 'wc-settings' === wp_unslash( $_REQUEST['page'] ) && isset( $_REQUEST['tab'] ) && 'checkout' === wp_unslash( $_REQUEST['tab'] ) && ! isset( $_REQUEST['section'] ) && is_admin(); // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } } includes/admin/class-wc-rest-payments-transactions-controller.php 0000777 00000024035 15251156605 0021410 0 ustar 00 <?php /** * Class WC_REST_Payments_Transactions_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request\List_Transactions; use WCPay\Core\Server\Request\List_Fraud_Outcome_Transactions; defined( 'ABSPATH' ) || exit; /** * REST controller for transactions. */ class WC_REST_Payments_Transactions_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/transactions'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_transactions' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/download', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_transactions_export' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/download/(?P<export_id>.*)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_export_url' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/summary', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_transactions_summary' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/search', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_transactions_search_autocomplete' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/fraud-outcomes', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_fraud_outcome_transactions' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/fraud-outcomes/summary', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_fraud_outcome_transactions_summary' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/fraud-outcomes/search', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_fraud_outcome_transactions_search_autocomplete' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/fraud-outcomes/download', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_fraud_outcome_transactions_export' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve transactions to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_transactions( $request ) { $wcpay_request = List_Transactions::from_rest_request( $request ); return $wcpay_request->handle_rest_request(); } /** * Retrieve fraud outcome transactions to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_fraud_outcome_transactions( $request ) { $wcpay_request = List_Fraud_Outcome_Transactions::from_rest_request( $request ); return $this->forward_request( 'list_fraud_outcome_transactions', [ $wcpay_request ] ); } /** * Retrieve fraud outcome transactions summary to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_fraud_outcome_transactions_summary( $request ) { $wcpay_request = List_Fraud_Outcome_Transactions::from_rest_request( $request ); return $this->forward_request( 'list_fraud_outcome_transactions_summary', [ $wcpay_request ] ); } /** * Retrieve transactions search options to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_fraud_outcome_transactions_search_autocomplete( $request ) { $wcpay_request = List_Fraud_Outcome_Transactions::from_rest_request( $request ); return $this->forward_request( 'get_fraud_outcome_transactions_search_autocomplete', [ $wcpay_request ] ); } /** * Initiate transactions export via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_fraud_outcome_transactions_export( $request ) { $wcpay_request = List_Fraud_Outcome_Transactions::from_rest_request( $request ); return $this->forward_request( 'get_fraud_outcome_transactions_export', [ $wcpay_request ] ); } /** * Initiate transactions export via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_transactions_export( $request ) { $user_email = $request->get_param( 'user_email' ); $deposit_id = $request->get_param( 'deposit_id' ); $locale = $request->get_param( 'locale' ); $filters = $this->get_transactions_filters( $request ); return $this->forward_request( 'get_transactions_export', [ $filters, $user_email, $deposit_id, $locale ] ); } /** * Get the export URL for a given export ID, if available. * * @param WP_REST_Request $request Full data about the request. */ public function get_export_url( $request ) { $export_id = $request->get_param( 'export_id' ); return $this->forward_request( 'get_transactions_export_url', [ $export_id ] ); } /** * Retrieve transactions summary to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_transactions_summary( $request ) { $deposit_id = $request->get_param( 'deposit_id' ); $filters = $this->get_transactions_filters( $request ); return $this->forward_request( 'get_transactions_summary', [ $filters, $deposit_id ] ); } /** * Retrieve transactions search options to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_transactions_search_autocomplete( $request ) { $search_term = $request->get_param( 'search_term' ); return $this->forward_request( 'get_transactions_search_autocomplete', [ $search_term ] ); } /** * Extract transactions filters from request * * @param WP_REST_Request $request Full data about the request. */ private function get_transactions_filters( $request ) { $date_between_filter = $request->get_param( 'date_between' ); $user_timezone = $request->get_param( 'user_timezone' ); if ( ! is_null( $date_between_filter ) ) { $date_between_filter = array_map( function ( $transaction_date ) use ( $user_timezone ) { return $this->format_transaction_date_with_timestamp( $transaction_date, $user_timezone ); }, $date_between_filter ); } return array_filter( [ 'match' => $request->get_param( 'match' ), 'date_before' => $this->format_transaction_date_with_timestamp( $request->get_param( 'date_before' ), $user_timezone ), 'date_after' => $this->format_transaction_date_with_timestamp( $request->get_param( 'date_after' ), $user_timezone ), 'date_between' => $date_between_filter, 'type_is' => $request->get_param( 'type_is' ), 'type_is_not' => $request->get_param( 'type_is_not' ), 'source_device_is' => $request->get_param( 'source_device_is' ), 'source_device_is_not' => $request->get_param( 'source_device_is_not' ), 'channel_is' => $request->get_param( 'channel_is' ), 'channel_is_not' => $request->get_param( 'channel_is_not' ), 'customer_country_is' => $request->get_param( 'customer_country_is' ), 'customer_country_is_not' => $request->get_param( 'customer_country_is_not' ), 'risk_level_is' => $request->get_param( 'risk_level_is' ), 'risk_level_is_not' => $request->get_param( 'risk_level_is_not' ), 'store_currency_is' => $request->get_param( 'store_currency_is' ), 'customer_currency_is' => $request->get_param( 'customer_currency_is' ), 'customer_currency_is_not' => $request->get_param( 'customer_currency_is_not' ), 'source_is' => $request->get_param( 'source_is' ), 'source_is_not' => $request->get_param( 'source_is_not' ), 'loan_id_is' => $request->get_param( 'loan_id_is' ), 'search' => $request->get_param( 'search' ), ], static function ( $filter ) { return null !== $filter; } ); } /** * Formats the incoming transaction date as per the blog's timezone. * * @param string|null $transaction_date Transaction date to format. * @param string|null $user_timezone User's timezone passed from client. * * @return string|null The formatted transaction date as per timezone. */ private function format_transaction_date_with_timestamp( $transaction_date, $user_timezone ) { if ( is_null( $transaction_date ) || is_null( $user_timezone ) ) { return $transaction_date; } // Get blog timezone. $blog_time = new DateTime( $transaction_date ); $blog_time->setTimezone( new DateTimeZone( wp_timezone_string() ) ); // Get local timezone. $local_time = new DateTime( $transaction_date ); $local_time->setTimezone( new DateTimeZone( $user_timezone ) ); // Compute time difference in minutes. $time_difference = ( strtotime( $local_time->format( 'Y-m-d H:i:s' ) ) - strtotime( $blog_time->format( 'Y-m-d H:i:s' ) ) ) / 60; // Shift date by time difference. $formatted_date = new DateTime( $transaction_date ); date_modify( $formatted_date, $time_difference . 'minutes' ); return $formatted_date->format( 'Y-m-d H:i:s' ); } } includes/admin/tracks/tracks-loader.php 0000777 00000002434 15251156605 0014215 0 ustar 00 <?php /** * Class Core Tracks Wrapper * * @package WooCommerce\Payments */ namespace WCPay; use WC_Tracks; defined( 'ABSPATH' ) || exit; // block direct access. /** * Moves all events from Tracker to Tracks loader */ function record_tracker_events() { foreach ( Tracker::get_admin_events() as $event => $properties ) { WC_Tracks::record_event( $event, $properties ); Tracker::remove_admin_event( $event ); } } // Loaded on admin_init to ensure that we are in admin and that WC_Tracks is loaded. add_action( 'admin_init', function () { if ( ! class_exists( 'WC_Tracks' ) ) { return; } // Move all events with priority 1 just before the admin_footer hook adds footer pixels. add_action( 'admin_footer', 'WCPay\record_tracker_events', 1 ); /** * Send all events that were not handled in `admin_footer`. * * Between shutdown and admin footer many things can happen. Admin footer loads * scripts in the markup images that will call tracks from the browsers * side (which means it's faster as we're not doing network calls to wp.com server * side). Anything that is added afterward must be sent from the * server, but doing it on shutdown means it's not blocking anything. */ add_action( 'shutdown', __NAMESPACE__ . '\\record_tracker_events', 1 ); } ); includes/admin/tracks/class-tracker.php 0000777 00000002410 15251156605 0014212 0 ustar 00 <?php /** * Class Tracker * * @package WooCommerce\Payments */ namespace WCPay; defined( 'ABSPATH' ) || exit; // block direct access. /** * An API for adding track events that will get unloaded * at a later stage and pushed to WP.com. */ class Tracker { /** * A key value array event_name => properties. * * @var array $admin_events */ protected static $admin_events = []; /** * Record an event in Tracks * * This only sends track events for admin logged in users. This is a limitation of the * WC_Tracks and related classes. * * The event name will be prefixed before sending it see Core_Tracks_Wrapper. * * @param string $event_name The name of the event. * @param array $properties Custom properties to send with the event. */ public static function track_admin( $event_name, $properties = [] ) { self::$admin_events[ $event_name ] = $properties; } /** * Remove a track event. * * @param string $event_name The name of the event that should be removed. */ public static function remove_admin_event( $event_name ) { if ( isset( self::$admin_events ) ) { unset( self::$admin_events[ $event_name ] ); } } /** * Remove a track event. */ public static function get_admin_events() { return self::$admin_events; } } includes/admin/class-wc-rest-payments-payment-intents-controller.php 0000777 00000035046 15251156605 0022043 0 ustar 00 <?php /** * Class WC_REST_Payments_Payment_Intents_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request\Create_And_Confirm_Intention; use WCPay\Logger; use WCPay\Exceptions\Rest_Request_Exception; use WCPay\Constants\Payment_Type; use WCPay\Internal\Service\Level3Service; use WCPay\Internal\Service\OrderService; defined( 'ABSPATH' ) || exit; /** * REST controller for charges. */ class WC_REST_Payments_Payment_Intents_Controller extends WC_Payments_REST_Controller { /** * Instance of WC_Payment_Gateway_WCPay * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * Order service instance. * * @var OrderService */ private $order_service; /** * Level3 service instance. * * @var Level3Service */ private $level3_service; /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/payment_intents'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<payment_intent_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_payment_intent' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_payment_intent' ], 'permission_callback' => [ $this, 'check_permission' ], 'schema' => [ $this, 'get_item_schema' ], ] ); } /** * WC_REST_Payments_Payment_Intents_Controller constructor. * * @param WC_Payments_API_Client $api_client WooCommerce Payments API client. * @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway. * @param OrderService $order_service The new order service. * @param Level3Service $level3_service Level3 service instance. */ public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, OrderService $order_service, Level3Service $level3_service ) { parent::__construct( $api_client ); $this->gateway = $gateway; $this->order_service = $order_service; $this->level3_service = $level3_service; } /** * Retrieve charge to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_payment_intent( $request ) { $payment_intent_id = $request->get_param( 'payment_intent_id' ); return $this->forward_request( 'get_intent', [ $payment_intent_id ] ); } /** * Create a payment intent. * * @param WP_REST_Request $request data about the request. * * @throws Rest_Request_Exception */ public function create_payment_intent( $request ) { try { $order_id = $request->get_param( 'order_id' ); $order = wc_get_order( $order_id ); if ( ! $order ) { throw new Rest_Request_Exception( __( 'Order not found', 'woocommerce-payments' ) ); } $wcpay_server_request = Create_And_Confirm_Intention::create(); $currency = strtolower( $order->get_currency() ); $amount = WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ); $wcpay_server_request->set_currency_code( $currency ); $wcpay_server_request->set_amount( $amount ); $metadata = $this->order_service->get_payment_metadata( $order_id, Payment_Type::SINGLE() ); $wcpay_server_request->set_metadata( $metadata ); $wcpay_server_request->set_customer( $request->get_param( 'customer' ) ); $wcpay_server_request->set_level3( $this->level3_service->get_data_from_order( $order_id ) ); $wcpay_server_request->set_payment_method( $request->get_param( 'payment_method' ) ); $wcpay_server_request->set_payment_method_types( [ 'card' ] ); $wcpay_server_request->set_off_session( true ); $wcpay_server_request->set_capture_method( $this->gateway->get_option( 'manual_capture' ) && ( 'yes' === $this->gateway->get_option( 'manual_capture' ) ) ); $wcpay_server_request->assign_hook( 'wcpay_create_and_confirm_intent_request_api' ); $intent = $wcpay_server_request->send(); $response = $this->prepare_item_for_response( $intent, $request ); return rest_ensure_response( $this->prepare_response_for_collection( $response ) ); } catch ( \Throwable $e ) { Logger::error( 'Failed to create an intention via REST API: ' . $e ); return new WP_Error( 'wcpay_server_error', $e->getMessage(), [ 'status' => 500 ] ); } } /** * Item schema. * * @return array */ public function get_item_schema() { return [ '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'payment_intent', 'type' => 'object', 'properties' => [ 'id' => [ 'description' => __( 'ID for the payment intent.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'amount' => [ 'description' => __( 'The amount of the transaction.', 'woocommerce-payments' ), 'type' => 'integer', 'context' => [ 'view' ], ], 'currency' => [ 'description' => __( 'The currency of the transaction.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'created' => [ 'description' => __( 'The date when the payment intent was created.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'customer' => [ 'description' => __( 'The customer id of the intent', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'status' => [ 'description' => __( 'The status of the payment intent.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'charge' => [ 'description' => __( 'Charge object associated with this payment intention.', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'id' => [ 'description' => 'ID for the charge.', 'type' => 'string', 'context' => [ 'view' ], ], 'amount' => [ 'description' => 'The amount of the charge.', 'type' => 'integer', 'context' => [ 'view' ], ], 'payment_method_details' => [ 'description' => 'Details for the payment method used for the charge.', 'type' => 'object', 'properties' => [ 'card' => [ 'description' => 'Details for a card payment method.', 'type' => 'object', 'properties' => [ 'amount_authorized' => [ 'description' => 'The amount authorized by the card.', 'type' => 'integer', ], 'brand' => [ 'description' => 'The brand of the card.', 'type' => 'string', ], 'capture_before' => [ 'description' => 'Timestamp for when the authorization must be captured.', 'type' => 'string', ], 'country' => [ 'description' => 'The ISO country code.', 'type' => 'string', ], 'exp_month' => [ 'description' => 'The expiration month of the card.', 'type' => 'integer', ], 'exp_year' => [ 'description' => 'The expiration year of the card.', 'type' => 'integer', ], 'last4' => [ 'description' => 'The last 4 digits of the card.', 'type' => 'string', ], 'three_d_secure' => [ 'description' => 'Details for 3D Secure authentication.', 'type' => 'object', ], ], ], ], ], 'billing_details' => [ 'description' => __( 'Billing details for the payment method.', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'address' => [ 'description' => __( 'Address associated with the billing details.', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'city' => [ 'description' => __( 'City of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'country' => [ 'description' => __( 'Country of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'line1' => [ 'description' => __( 'Line 1 of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'line2' => [ 'description' => __( 'Line 2 of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'postal_code' => [ 'description' => __( 'Postal code of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'state' => [ 'description' => __( 'State of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ], 'email' => [ 'description' => __( 'Email associated with the billing details.', 'woocommerce-payments' ), 'type' => 'string', 'format' => 'email', 'context' => [ 'view' ], ], 'name' => [ 'description' => __( 'Name associated with the billing details.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'phone' => [ 'description' => __( 'Phone number associated with the billing details.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ], 'payment_method' => [ 'description' => 'The payment method associated with this charge.', 'type' => 'string', 'context' => [ 'view' ], ], 'application_fee_amount' => [ 'description' => 'The application fee amount.', 'type' => 'integer', 'context' => [ 'view' ], ], 'status' => [ 'description' => 'The status of the payment intent created.', 'type' => 'string', 'context' => [ 'view' ], ], ], ], ], ]; } /** * Prepare each item for response. * * @param array|mixed $item Item to prepare. * @param WP_REST_Request $request Request instance. * * @return WP_REST_Response|WP_Error */ public function prepare_item_for_response( $item, $request ) { $prepared_item = []; $prepared_item['id'] = $item->get_id(); $prepared_item['amount'] = $item->get_amount(); $prepared_item['currency'] = $item->get_currency(); $prepared_item['created'] = $item->get_created()->format( 'Y-m-d H:i:s' ); $prepared_item['customer'] = $item->get_customer_id(); $prepared_item['payment_method'] = $item->get_payment_method_id(); $prepared_item['status'] = $item->get_status(); try { $charge = $item->get_charge(); $prepared_item['charge']['id'] = $charge->get_id(); $prepared_item['charge']['amount'] = $charge->get_amount(); $prepared_item['charge']['application_fee_amount'] = $charge->get_application_fee_amount(); $prepared_item['charge']['status'] = $charge->get_status(); $billing_details = $charge->get_billing_details(); if ( isset( $billing_details['address'] ) ) { $prepared_item['charge']['billing_details']['address']['city'] = $billing_details['address']['city'] ?? ''; $prepared_item['charge']['billing_details']['address']['country'] = $billing_details['address']['country'] ?? ''; $prepared_item['charge']['billing_details']['address']['line1'] = $billing_details['address']['line1'] ?? ''; $prepared_item['charge']['billing_details']['address']['line2'] = $billing_details['address']['line2'] ?? ''; $prepared_item['charge']['billing_details']['address']['postal_code'] = $billing_details['address']['postal_code'] ?? ''; $prepared_item['charge']['billing_details']['address']['state'] = $billing_details['address']['state'] ?? ''; } $prepared_item['charge']['billing_details']['email'] = $billing_details['email'] ?? ''; $prepared_item['charge']['billing_details']['name'] = $billing_details['name'] ?? ''; $prepared_item['charge']['billing_details']['phone'] = $billing_details['phone'] ?? ''; $payment_method_details = $charge->get_payment_method_details(); if ( isset( $payment_method_details['card'] ) ) { $prepared_item['charge']['payment_method_details']['card']['amount_authorized'] = $payment_method_details['card']['amount_authorized'] ?? ''; $prepared_item['charge']['payment_method_details']['card']['brand'] = $payment_method_details['card']['brand'] ?? ''; $prepared_item['charge']['payment_method_details']['card']['capture_before'] = $payment_method_details['card']['capture_before'] ?? ''; $prepared_item['charge']['payment_method_details']['card']['country'] = $payment_method_details['card']['country'] ?? ''; $prepared_item['charge']['payment_method_details']['card']['exp_month'] = $payment_method_details['card']['exp_month'] ?? ''; $prepared_item['charge']['payment_method_details']['card']['exp_year'] = $payment_method_details['card']['exp_year'] ?? ''; $prepared_item['charge']['payment_method_details']['card']['last4'] = $payment_method_details['card']['last4'] ?? ''; $prepared_item['charge']['payment_method_details']['card']['three_d_secure'] = $payment_method_details['card']['three_d_secure'] ?? ''; } } catch ( \Throwable $e ) { Logger::error( 'Failed to prepare payment intent for response: ' . $e ); } $context = $request['context'] ?? 'view'; $prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request ); $prepared_item = $this->filter_response_by_context( $prepared_item, $context ); return rest_ensure_response( $prepared_item ); } } includes/admin/class-wc-payments-admin-settings.php 0000777 00000027650 15251156605 0016500 0 ustar 00 <?php /** * Class WC_Payments_Admin_Settings * * @package WooCommerce\Payments\Admin */ /** * WC_Payments_Admin_Settings class. */ class WC_Payments_Admin_Settings { /** * WC_Payment_Gateway_WCPay. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * Instance of WC_Payments_Account * * @var WC_Payments_Account */ private $account; /** * Set of parameters to build the URL to the gateway's settings page. * * @var string[] */ private static $settings_url_params = [ 'page' => 'wc-settings', 'tab' => 'checkout', 'section' => WC_Payment_Gateway_WCPay::GATEWAY_ID, ]; /** * Initialize class actions. * * @param WC_Payment_Gateway_WCPay $gateway Payment Gateway. * @param WC_Payments_Account $account The account service. */ public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Account $account ) { $this->gateway = $gateway; $this->account = $account; } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'maybe_show_test_mode_notice' ] ); add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'maybe_show_test_account_notice' ] ); add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'maybe_show_sandbox_account_notice' ] ); add_filter( 'plugin_action_links_' . plugin_basename( WCPAY_PLUGIN_FILE ), [ $this, 'add_plugin_links' ] ); add_action( 'admin_init', [ $this, 'maybe_redirect_payment_method_settings' ], 16 ); } /** * Add notice about payments being in test mode when using a live account. * * This notice is mutually exclusive with the test account and sandbox account notices. * * @see self::maybe_show_test_account_notice() * @see self::maybe_show_sandbox_account_notice() */ public function maybe_show_test_mode_notice() { // If there is no valid account connected, bail. if ( ! $this->gateway->is_connected() || ! $this->account->is_stripe_account_valid() ) { return; } // If this is not a live account, bail since we will inform the user about the test account instead. if ( ! $this->account->get_is_live() ) { return; } // If the test mode is not enabled, bail. if ( ! WC_Payments::mode()->is_test() ) { return; } // Output the notice. ?> <div id="wcpay-test-mode-notice" class="notice notice-warning"> <p> <b> <?php printf( /* translators: %s: WooPayments */ esc_html__( '%s is in test mode — all transactions are simulated!', 'woocommerce-payments' ) . ' ', 'WooPayments' ); ?> </b> <?php printf( /* translators: 1: Anchor opening tag; 2: Anchor closing tag */ esc_html__( 'You can use %1$stest card numbers%2$s to simulate various types of transactions.', 'woocommerce-payments' ), '<a href="' . esc_url( 'https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#test-cards' ) . '" target="_blank" rel="noreferrer noopener">', '</a>' ); ?> </p> </div> <?php } /** * Add notice to activate payments when a test account is in use. * * This notice is mutually exclusive with the test mode and sandbox account notices. * * @see self::maybe_show_test_mode_notice() * @see self::maybe_show_sandbox_account_notice() */ public function maybe_show_test_account_notice() { // If there is no valid account connected, bail. if ( ! $this->gateway->is_connected() || ! $this->account->is_stripe_account_valid() ) { return; } // If this is a live account, bail. if ( $this->account->get_is_live() ) { return; } // If this is NOT a test [drive] account, bail. $account_status = $this->account->get_account_status_data(); if ( empty( $account_status['testDrive'] ) ) { return; } // Output the notice. ?> <div id="wcpay-test-account-notice" class="notice notice-warning"> <p> <b><?php echo esc_html__( 'You are using a test account.', 'woocommerce-payments' ) . ' '; ?></b> <?php if ( ! WC_Payments::mode()->is_dev() ) { printf( /* translators: %s: URL to learn more */ esc_html__( 'Provide additional details about your business so you can begin accepting real payments. %1$sLearn more%2$s', 'woocommerce-payments' ), '<a href="' . esc_url( 'https://woocommerce.com/document/woopayments/startup-guide/#sign-up-process' ) . '" target="_blank" rel="noreferrer noopener">', '</a>' ); } else { esc_html_e( '⚠️ Development mode is enabled for the store! There can be no live onboarding process while using development, testing, or staging WordPress environments!', 'woocommerce-payments' ); echo '</br>'; printf( /* translators: 1: Anchor opening tag; 2: Anchor closing tag; 3: Anchor opening tag; 4: Anchor closing tag */ esc_html__( 'To begin accepting real payments, please go to the live store or change your %1$sWordPress environment%2$s to a production one. %3$sLearn more%4$s', 'woocommerce-payments' ), '<a href="' . esc_url( 'https://make.wordpress.org/core/2020/08/27/wordpress-environment-types/' ) . '" target="_blank" rel="noreferrer noopener">', '</a>', '<a href="' . esc_url( 'https://woocommerce.com/document/woopayments/testing-and-troubleshooting/test-accounts/#developer-notes' ) . '" target="_blank" rel="noreferrer noopener">', '</a>' ); } ?> </p> <?php if ( ! WC_Payments::mode()->is_dev() ) { ?> <p> <a id="wcpay-activate-payments-button" href="#" class="button button-secondary"> <?php esc_html_e( 'Activate payments', 'woocommerce-payments' ); ?> </a> </p> <?php } ?> </div> <?php if ( ! WC_Payments::mode()->is_dev() ) { ?> <script type="text/javascript"> // We dispatch an event to trigger the modal. // The listener is in the general-settings/index.js file. document.addEventListener( 'DOMContentLoaded', function () { var activateButton = document.getElementById( 'wcpay-activate-payments-button' ); if ( !activateButton ) { return; } activateButton.addEventListener( 'click', function ( e ) { e.preventDefault(); document.dispatchEvent( new CustomEvent( 'wcpay:activate_payments' ) ); } ); } ); </script> <?php } } /** * Add notice to inform that a sandbox account is in use. * * This notice is mutually exclusive with the test mode and test account notices. * * @see self::maybe_show_test_mode_notice() * @see self::maybe_show_test_account_notice() */ public function maybe_show_sandbox_account_notice() { // If there is no valid account connected, bail. if ( ! $this->gateway->is_connected() || ! $this->account->is_stripe_account_valid() ) { return; } // If this is a live account, bail. if ( $this->account->get_is_live() ) { return; } // If this is a test [drive] account, bail. $account_status = $this->account->get_account_status_data(); if ( ! empty( $account_status['testDrive'] ) ) { return; } // Output the notice. ?> <div id="wcpay-test-account-notice" class="notice notice-warning"> <p> <b><?php echo esc_html__( 'You are using a sandbox test account.', 'woocommerce-payments' ) . ' '; ?></b> <?php if ( ! WC_Payments::mode()->is_dev() ) { printf( /* translators: 1: Anchor opening tag; 2: Anchor closing tag; 3: Anchor opening tag; 4: Anchor closing tag */ esc_html__( 'To begin accepting real payments you will need to first %1$sreset your account%2$s and, then, provide additional details about your business. %3$sLearn more%4$s', 'woocommerce-payments' ), '<a href="' . esc_url( 'https://woocommerce.com/document/woopayments/startup-guide/#resetting' ) . '" target="_blank" rel="noreferrer noopener">', '</a>', '<a href="' . esc_url( 'https://woocommerce.com/document/woopayments/startup-guide/#sign-up-process' ) . '" target="_blank" rel="noreferrer noopener">', '</a>', ); } else { esc_html_e( '⚠️ Development mode is enabled for the store! There can be no live onboarding process while using development, testing, or staging WordPress environments!', 'woocommerce-payments' ); echo '</br>'; printf( /* translators: 1: Anchor opening tag; 2: Anchor closing tag; 3: Anchor opening tag; 4: Anchor closing tag */ esc_html__( 'To begin accepting real payments, please go to the live store or change your %1$sWordPress environment%2$s to a production one. %3$sLearn more%4$s', 'woocommerce-payments' ), '<a href="' . esc_url( 'https://make.wordpress.org/core/2020/08/27/wordpress-environment-types/' ) . '" target="_blank" rel="noreferrer noopener">', '</a>', '<a href="' . esc_url( 'https://woocommerce.com/document/woopayments/testing-and-troubleshooting/test-accounts/#developer-notes' ) . '" target="_blank" rel="noreferrer noopener">', '</a>' ); } ?> </p> </div> <?php } /** * Redirect payment-method-specific settings URLs to the main WooPayments settings page. * WooCommerce Core generates emails when a new payment method is enabled, with links to the payment method's name. * Since all WooPayments payment methods are managed in the main WooPayments settings page, we need to redirect the merchant to the main settings page. * * Handles URLs like: * - /wp-admin/admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments_klarna * - /wp-admin/admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments_wechat_pay * And redirects them to: * - /wp-admin/admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments * * @return void */ public function maybe_redirect_payment_method_settings(): void { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return; } $url_params = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification if ( empty( $url_params['page'] ) || 'wc-settings' !== $url_params['page'] ) { return; } if ( empty( $url_params['tab'] ) || 'checkout' !== $url_params['tab'] ) { return; } if ( empty( $url_params['section'] ) ) { return; } // is this a payment-method-specific URL? (e.g.: woocommerce_payments_klarna, woocommerce_payments_wechat_pay). $section = $url_params['section']; if ( 0 !== strpos( $section, 'woocommerce_payments_' ) ) { return; } $redirect_result = wp_safe_redirect( self::get_settings_url() ); // in unit tests, we're not redirecting and `wp_safe_redirect` returns `false`. We can't just `exit` in unit tests. if ( true === $redirect_result ) { exit; } } /** * Adds links to the plugin's row in the "Plugins" Wp-Admin page. * * @see https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name) * @param array $links The existing list of links that will be rendered. * @return array The list of links that will be rendered, after adding some links specific to this plugin. */ public function add_plugin_links( $links ) { $plugin_links = [ '<a href="' . esc_attr( self::get_settings_url() ) . '">' . esc_html__( 'Settings', 'woocommerce-payments' ) . '</a>', ]; return array_merge( $plugin_links, $links ); } /** * Whether the current page is the WooPayments settings page. * * @return bool */ public static function is_current_page_settings() { return count( self::$settings_url_params ) === count( array_intersect_assoc( $_GET, self::$settings_url_params ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } /** * Returns the URL of the configuration screen for this gateway, for use in internal links. * * @param array $query_args Optional additional query args to append to the URL. * * @return string URL of the configuration screen for this gateway */ public static function get_settings_url( $query_args = [] ) { return admin_url( add_query_arg( array_merge( self::$settings_url_params, $query_args ), 'admin.php' ) ); // nosemgrep: audit.php.wp.security.xss.query-arg -- constant string is passed in. } } includes/admin/class-wc-rest-payments-onboarding-controller.php 0000777 00000025742 15251156605 0021030 0 ustar 00 <?php /** * Class WC_REST_Payments_Onboarding_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; /** * REST controller for account details and status. */ class WC_REST_Payments_Onboarding_Controller extends WC_Payments_REST_Controller { const RESULT_BAD_REQUEST = 'bad_request'; /** * Onboarding Service. * * @var WC_Payments_Onboarding_Service */ protected $onboarding_service; /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/onboarding'; /** * Constructor. * * @param WC_Payments_API_Client $api_client WooCommerce Payments API client. * @param WC_Payments_Onboarding_Service $onboarding_service Onboarding Service class instance. */ public function __construct( WC_Payments_API_Client $api_client, WC_Payments_Onboarding_Service $onboarding_service ) { parent::__construct( $api_client ); $this->onboarding_service = $onboarding_service; } /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/kyc/session', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_embedded_kyc_session' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'self_assessment' => [ 'required' => false, 'description' => 'The self-assessment data.', 'type' => 'object', 'properties' => [ 'country' => [ 'type' => 'string', 'description' => 'The country code where the company is legally registered.', ], 'business_type' => [ 'type' => 'string', 'description' => 'The company incorporation type.', ], 'mcc' => [ 'type' => 'string', 'description' => 'The merchant category code. This can either be a true MCC or an MCCs tree item id from the onboarding form.', ], 'site' => [ 'type' => 'string', 'description' => 'The URL of the site.', ], ], ], 'capabilities' => [ 'description' => 'The capabilities to request and enable for the test-drive account. Leave empty to use the default capabilities.', 'type' => 'object', 'default' => [], 'required' => false, 'properties' => [ '*' => [ 'type' => 'boolean', ], ], ], ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/kyc/finalize', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'finalize_embedded_kyc' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'source' => [ 'required' => false, 'description' => 'The very first entry point the merchant entered our onboarding flow.', 'type' => 'string', ], 'from' => [ 'required' => false, 'description' => 'The previous step in the onboarding flow leading the merchant to arrive at the current step.', 'type' => 'string', ], ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/reset', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'reset_onboarding' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'source' => [ 'required' => false, 'description' => 'The very first entry point the merchant entered our onboarding flow.', 'type' => 'string', ], 'from' => [ 'required' => false, 'description' => 'The previous step in the onboarding flow leading the merchant to arrive at the current step.', 'type' => 'string', ], ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/fields', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_fields' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/business_types', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_business_types' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/test_drive_account/init', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'init_test_drive_account' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'country' => [ 'type' => 'string', 'description' => 'The country code for which to create the test-drive account.', 'required' => false, ], 'capabilities' => [ 'description' => 'The capabilities to request and enable for the test-drive account. Leave empty to use the default capabilities.', 'type' => 'object', 'default' => [], 'required' => false, 'properties' => [ '*' => [ 'type' => 'boolean', ], ], ], ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/test_drive_account/disable', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'disable_test_drive_account' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'source' => [ 'required' => false, 'description' => 'The very first entry point the merchant entered our onboarding flow.', 'type' => 'string', ], 'from' => [ 'required' => false, 'description' => 'The previous step in the onboarding flow leading the merchant to arrive at the current step.', 'type' => 'string', ], ], ] ); } /** * Create an account embedded KYC session via the API. * * @param WP_REST_Request $request Request object. * * @return WP_Error|WP_REST_Response Response object containing the account session, or an error if session creation failed. */ public function create_embedded_kyc_session( WP_REST_Request $request ) { $self_assessment_data = ! empty( $request->get_param( 'self_assessment' ) ) ? wc_clean( wp_unslash( $request->get_param( 'self_assessment' ) ) ) : []; $capabilities = ! empty( $request->get_param( 'capabilities' ) ) ? wc_clean( wp_unslash( $request->get_param( 'capabilities' ) ) ) : []; $account_session = $this->onboarding_service->create_embedded_kyc_session( $self_assessment_data, $capabilities ); if ( empty( $account_session ) ) { WC_Payments_Utils::log_to_wc( 'Failed to create embedded KYC session: Empty response from onboarding service.' ); } elseif ( empty( $account_session['publishableKey'] ) ) { WC_Payments_Utils::log_to_wc( sprintf( 'Embedded KYC session missing publishableKey. Session keys: %s.', implode( ', ', array_keys( $account_session ) ) ), 'warning' ); } if ( $account_session ) { $account_session['locale'] = get_user_locale(); } return rest_ensure_response( $account_session ); } /** * Finalize the embedded KYC session via the API. * * @param WP_REST_Request $request Request object. * * @return WP_REST_Response|WP_Error */ public function finalize_embedded_kyc( WP_REST_Request $request ) { $source = $request->get_param( 'source' ) ?? ''; $from = $request->get_param( 'from' ) ?? ''; $actioned_notes = WC_Payments_Onboarding_Service::get_actioned_notes(); // Call the API to finalize the onboarding. try { $response = $this->onboarding_service->finalize_embedded_kyc( get_user_locale(), $source, $actioned_notes ); } catch ( Exception $e ) { return new WP_Error( self::RESULT_BAD_REQUEST, $e->getMessage(), [ 'status' => 400 ] ); } // Handle some post-onboarding tasks and get the redirect params. $finalize = WC_Payments::get_account_service()->finalize_embedded_connection( $response['mode'], [ 'promo' => $response['promotion_id'] ?? '', 'from' => $from, 'source' => $source, ] ); // Return the response, the client will handle the redirect. return rest_ensure_response( array_merge( $response, $finalize ) ); } /** * Reset the onboarding via the API. * * @param WP_REST_Request $request Request object. * * @return WP_REST_Response|WP_Error */ public function reset_onboarding( WP_REST_Request $request ) { $context = [ 'from' => $request->get_param( 'from' ) ?? '', 'source' => $request->get_param( 'source' ) ?? '', ]; try { $result = $this->onboarding_service->reset_onboarding( $context ); } catch ( Exception $e ) { return new WP_Error( self::RESULT_BAD_REQUEST, $e->getMessage(), [ 'status' => 400 ] ); } return rest_ensure_response( [ 'success' => $result ] ); } /** * Get fields data via API. * * @return WP_REST_Response|WP_Error */ public function get_fields() { $fields = $this->onboarding_service->get_fields_data( get_user_locale() ); if ( is_null( $fields ) ) { return new WP_Error( self::RESULT_BAD_REQUEST, 'Failed to retrieve the onboarding fields.', [ 'status' => 400 ] ); } return rest_ensure_response( [ 'data' => $fields ] ); } /** * Get business types via API. * * @return WP_REST_Response|WP_Error */ public function get_business_types() { $business_types = $this->onboarding_service->get_cached_business_types(); return rest_ensure_response( [ 'data' => $business_types ] ); } /** * Initialize a test-drive account. * * @param WP_REST_Request $request Request object. * * @return WP_REST_Response|WP_Error */ public function init_test_drive_account( WP_REST_Request $request ) { $country = $request->get_param( 'country' ); if ( empty( $country ) ) { // Fall back to the store's base country if no country is provided. $country = WC()->countries->get_base_country() ?? 'US'; } try { $success = $this->onboarding_service->init_test_drive_account( $country, $request->get_param( 'capabilities' ) ); } catch ( Exception $e ) { return new WP_Error( self::RESULT_BAD_REQUEST, $e->getMessage(), [ 'status' => 400 ] ); } return rest_ensure_response( [ 'success' => $success, ] ); } /** * Disable Test Drive account API. * * @param WP_REST_Request $request Request object. * * @return WP_REST_Response|WP_Error */ public function disable_test_drive_account( WP_REST_Request $request ) { $context = [ 'from' => $request->get_param( 'from' ) ?? '', 'source' => $request->get_param( 'source' ) ?? '', ]; try { $result = $this->onboarding_service->disable_test_drive_account( $context ); } catch ( Exception $e ) { return new WP_Error( self::RESULT_BAD_REQUEST, $e->getMessage(), [ 'status' => 400 ] ); } return rest_ensure_response( [ 'success' => $result ] ); } } includes/admin/class-wc-rest-payments-reader-controller.php 0000777 00000027203 15251156605 0020142 0 ustar 00 <?php /** * Class WC_REST_Payments_Reader_Charges * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request\Get_Charge; use WCPay\Core\Server\Request\Get_Intention; use WCPay\Constants\Intent_Status; use WCPay\Core\Server\Request; use WCPay\Exceptions\API_Exception; defined( 'ABSPATH' ) || exit; require_once WCPAY_ABSPATH . 'includes/in-person-payments/class-wc-payments-printed-receipt-sample-order.php'; /** * REST controller for reader charges. */ class WC_REST_Payments_Reader_Controller extends WC_Payments_REST_Controller { const STORE_READERS_TRANSIENT_KEY = 'wcpay_store_terminal_readers'; const PREVIEW_RECEIPT_CHARGE_DATA = [ 'amount_captured' => 0, 'payment_method_details' => [ 'card_present' => [ 'brand' => 'Sample', 'last4' => '0000', 'receipt' => [ 'application_preferred_name' => 'Sample, Receipts preview', 'dedicated_file_name' => '0000', 'account_type' => 'Sample', ], ], ], ]; /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/readers'; /** * Instance of WC_Payment_Gateway_WCPay. * * @var WC_Payment_Gateway_WCPay */ private $wcpay_gateway; /** * Instance of WC_Payments_In_Person_Payments_Receipts_Service. * * @var WC_Payments_In_Person_Payments_Receipts_Service */ private $receipts_service; /** * WC_REST_Payments_Reader_Controller * * @param WC_Payments_API_Client $api_client WC_Payments_API_Client. * @param WC_Payment_Gateway_WCPay $wcpay_gateway WC_Payment_Gateway_WCPay. * @param WC_Payments_In_Person_Payments_Receipts_Service $receipts_service WC_Payments_In_Person_Payments_Receipts_Service. * @return void */ public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $wcpay_gateway, WC_Payments_In_Person_Payments_Receipts_Service $receipts_service ) { parent::__construct( $api_client ); $this->wcpay_gateway = $wcpay_gateway; $this->receipts_service = $receipts_service; } /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_all_readers' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'register_reader' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'location' => [ 'type' => 'string', 'required' => true, ], 'registration_code' => [ 'type' => 'string', 'required' => true, ], 'label' => [ 'type' => 'string', ], 'metadata' => [ 'type' => 'object', ], ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/charges/(?P<transaction_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_summary' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/receipts/preview', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'preview_print_receipt' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/receipts/(?P<payment_intent_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'generate_print_receipt' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve payment readers charges to respond with via API. * * @param WP_REST_Request $request Full data about the request. * * @return WP_REST_Response|WP_Error */ public function get_summary( $request ) { $transaction_id = $request->get_param( 'transaction_id' ); try { // retrieve transaction details to get the charge date. $transaction = $this->api_client->get_transaction( $transaction_id ); if ( empty( $transaction ) ) { return rest_ensure_response( [] ); } $summary = $this->api_client->get_readers_charge_summary( gmdate( 'Y-m-d', $transaction['created'] ), $transaction_id ); } catch ( API_Exception $e ) { return rest_ensure_response( new WP_Error( 'wcpay_get_summary', $e->getMessage() ) ); } return rest_ensure_response( $summary ); } /** * Proxies the get all readers request to the server. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function get_all_readers( $request ) { try { return rest_ensure_response( $this->fetch_readers() ); } catch ( API_Exception $e ) { return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage() ) ); } } /** * Links a card reader to an account and terminal location. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function register_reader( $request ) { try { $response = $this->api_client->register_terminal_reader( $request->get_param( 'location' ), $request->get_param( 'registration_code' ), $request->get_param( 'label' ), $request->get_param( 'metadata' ) ); $reader = wp_array_slice_assoc( $response, [ 'id', 'livemode', 'device_type', 'label', 'location', 'metadata', 'status' ] ); return rest_ensure_response( $reader ); } catch ( API_Exception $e ) { return rest_ensure_response( new WP_Error( $e->get_error_code(), $e->getMessage(), [ 'status' => $e->get_http_code() ] ) ); } } /** * Check if the reader status is active * * @param array $readers The readers charges object. * @param string $id The reader ID. * @return bool */ private function is_reader_active( $readers, $id ) { foreach ( $readers as $reader ) { if ( $reader['reader_id'] === $id && 'active' === $reader['status'] ) { return true; } } return false; } /** * Attempts to read readers from transient and re-populates it if needed. * * @return array Terminal readers. * @throws API_Exception If request to server fails. */ private function fetch_readers(): array { $readers = get_transient( static::STORE_READERS_TRANSIENT_KEY ); if ( ! $readers ) { // Retrieve terminal readers. $request = Request::get( WC_Payments_API_Client::TERMINAL_READERS_API ); $request->assign_hook( 'wcpay_get_terminal_readers_request' ); $readers_data = $request->send(); // Retrieve the readers by charges. $reader_by_charges = $this->api_client->get_readers_charge_summary( gmdate( 'Y-m-d', time() ) ); $readers = []; foreach ( $readers_data as $reader ) { $readers[] = [ 'id' => $reader['id'], 'livemode' => $reader['livemode'], 'device_type' => $reader['device_type'], 'label' => $reader['label'], 'location' => $reader['location'], 'metadata' => $reader['metadata'], 'status' => $reader['status'], 'is_active' => $this->is_reader_active( $reader_by_charges, $reader['id'] ), ]; } set_transient( static::STORE_READERS_TRANSIENT_KEY, $readers, 2 * HOUR_IN_SECONDS ); } return $readers; } /** * Renders HTML for a print receipt * * @param WP_REST_Request $request Full data about the request. * @return WP_HTTP_Response|WP_Error * @throws \RuntimeException Error collecting data. */ public function generate_print_receipt( $request ) { try { /* Collect the data, available on the server side. */ $wcpay_request = Get_Intention::create( $request->get_param( 'payment_intent_id' ) ); $payment_intent = $wcpay_request->send(); if ( Intent_Status::SUCCEEDED !== $payment_intent->get_status() ) { throw new \RuntimeException( __( 'Invalid payment intent', 'woocommerce-payments' ) ); } $charge = $payment_intent->get_charge(); $charge_id = $charge ? $charge->get_id() : null; $charge_request = Get_Charge::create( $charge_id ); $charge_array = $charge_request->send(); /* Collect receipt data, stored on the store side. */ $order = wc_get_order( $charge_array['order']['number'] ); if ( false === $order ) { throw new \RuntimeException( __( 'Order not found', 'woocommerce-payments' ) ); } // Retrieve branding logo file ID. $branding_logo = $this->wcpay_gateway->get_option( 'account_branding_logo', '' ); /* Collect merchant settings */ $settings = [ 'branding_logo' => ( ! empty( $branding_logo ) ) ? $this->api_client->get_file_contents( $branding_logo, false ) : [], 'business_name' => $this->wcpay_gateway->get_option( 'account_business_name' ), 'support_info' => [ 'address' => $this->wcpay_gateway->get_option( 'account_business_support_address' ), 'phone' => $this->wcpay_gateway->get_option( 'account_business_support_phone' ), 'email' => $this->wcpay_gateway->get_option( 'account_business_support_email' ), ], ]; /* Generate receipt */ $response = [ 'html_content' => $this->receipts_service->get_receipt_markup( $settings, $order, $charge_array ) ]; } catch ( \Throwable $e ) { $error_status_code = $e instanceof API_Exception ? $e->get_http_code() : 500; $response = new WP_Error( 'generate_print_receipt_error', $e->getMessage(), [ 'status' => $error_status_code ] ); } return rest_ensure_response( $response ); } /** * Returns HTML to preview a print receipt * * @param WP_REST_Request $request Full data about the request. * @return WP_HTTP_Response|WP_Error * @throws \RuntimeException Error collecting data. */ public function preview_print_receipt( WP_REST_Request $request ) { $preview = $this->receipts_service->get_receipt_markup( $this->create_print_preview_receipt_settings_data( $request->get_json_params() ), new WC_Payments_Printed_Receipt_Sample_Order(), self::PREVIEW_RECEIPT_CHARGE_DATA ); return rest_ensure_response( [ 'html_content' => $preview ] ); } /** * Creates settings data to be used on the printed receipt preview. Defaults to stored settings if one parameter is not provided. * * @param array $receipt_settings Array of settings to use to create the receipt preview. * @return array */ private function create_print_preview_receipt_settings_data( array $receipt_settings ): array { $support_address = empty( $receipt_settings['accountBusinessSupportAddress'] ) ? $this->wcpay_gateway->get_option( 'account_business_support_address' ) : $receipt_settings['accountBusinessSupportAddress']; return [ 'business_name' => empty( $receipt_settings['accountBusinessName'] ) ? $this->wcpay_gateway->get_option( 'account_business_name' ) : $receipt_settings['accountBusinessName'], 'support_info' => [ 'address' => [ 'line1' => $support_address['line1'], 'line2' => $support_address['line2'], 'city' => $support_address['city'], 'state' => $support_address['state'], 'postal_code' => $support_address['postal_code'], 'country' => $support_address['country'], ], 'phone' => empty( $receipt_settings['accountBusinessSupportPhone'] ) ? $this->wcpay_gateway->get_option( 'account_business_support_phone' ) : $receipt_settings['accountBusinessSupportPhone'], 'email' => empty( $receipt_settings['accountBusinessSupportEmail'] ) ? $this->wcpay_gateway->get_option( 'account_business_support_email' ) : $receipt_settings['accountBusinessSupportEmail'], ], ]; } } includes/admin/class-wc-rest-upe-flag-toggle-controller.php 0000777 00000000600 15251156605 0020011 0 ustar 00 <?php /** * Class WC_REST_UPE_Flag_Toggle_Controller * * @package WooCommerce\Payments\Admin */ /** * REST controller for UPE feature flag. Needs to stay in the codebase to avoid error on plugin update for versions 6.9.2 or earlier. */ class WC_REST_UPE_Flag_Toggle_Controller extends WP_REST_Controller { /** * Register routes. */ public function register_routes() {} } includes/admin/class-wc-rest-payments-vat-controller.php 0000777 00000004375 15251156605 0017477 0 ustar 00 <?php /** * Class WC_REST_Payments_VAT_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request; use WCPay\Exceptions\API_Exception; defined( 'ABSPATH' ) || exit; /** * REST controller for vat. */ class WC_REST_Payments_VAT_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/vat'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<vat_number>[\w\.\%]+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'validate_vat' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::EDITABLE, 'args' => [ 'vat_number' => [ 'type' => 'string', 'format' => 'text-field', 'required' => false, ], 'name' => [ 'type' => 'string', 'format' => 'text-field', 'required' => true, ], 'address' => [ 'type' => 'string', 'format' => 'textarea-field', 'required' => true, ], ], 'callback' => [ $this, 'save_vat_details' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Validate VAT number to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function validate_vat( $request ) { $vat_number = sanitize_text_field( $request->get_param( 'vat_number' ) ); $server_request = Request::get( WC_Payments_API_Client::VAT_API, $vat_number ); $server_request->assign_hook( 'wcpay_validate_vat_request' ); return $server_request->handle_rest_request(); } /** * Save VAT details and respond via API. * * @param WP_REST_Request $request Full data about the request. */ public function save_vat_details( $request ) { $vat_number = $request->get_param( 'vat_number' ); $name = $request->get_param( 'name' ); $address = $request->get_param( 'address' ); return $this->forward_request( 'save_vat_details', [ $vat_number, $name, $address ] ); } } includes/admin/class-wc-rest-payments-connection-tokens-controller.php 0000777 00000002444 15251156605 0022340 0 ustar 00 <?php /** * Class WC_REST_Payments_Connection_Tokens_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; /** * REST controller for terminal tokens. */ class WC_REST_Payments_Connection_Tokens_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/connection_tokens'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/payments/connection_tokens', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_token' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Create a connection token via API. * * @param WP_REST_Request $request Full data about the request. */ public function create_token( $request ) { $response = $this->forward_request( 'create_token', [ $request ] ); // As an aid to mobile clients, tuck in the test_mode flag in the response returned to the request. if ( is_a( $response, 'WP_REST_Response' ) ) { if ( property_exists( $response, 'data' ) ) { if ( is_array( $response->data ) ) { $response->data['test_mode'] = WC_Payments::mode()->is_test(); } } } return $response; } } includes/admin/class-wc-rest-payments-disputes-controller.php 0000777 00000013104 15251156605 0020533 0 ustar 00 <?php /** * Class WC_REST_Payments_Disputes_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request\List_Disputes; defined( 'ABSPATH' ) || exit; /** * REST controller for disputes. */ class WC_REST_Payments_Disputes_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/disputes'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_disputes' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/download/(?P<export_id>.*)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_export_url' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/summary', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_disputes_summary' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/download', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_disputes_export' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<dispute_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_dispute' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<dispute_id>\w+)', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'update_dispute' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<dispute_id>\w+)/close', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'close_dispute' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve disputes to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_disputes( WP_REST_Request $request ) { $wcpay_request = List_Disputes::from_rest_request( $request ); return $wcpay_request->handle_rest_request(); } /** * Get the disputes export URL for a given export ID, if available. * * @param WP_REST_Request $request Full data about the request. */ public function get_export_url( $request ) { $export_id = $request->get_param( 'export_id' ); return $this->forward_request( 'get_disputes_export_url', [ $export_id ] ); } /** * Retrieve disputes summary to respond with via API. * * @param WP_REST_Request $request Request data. * @return WP_REST_Response|WP_Error */ public function get_disputes_summary( WP_REST_Request $request ) { $filters = $this->get_disputes_filters( $request ); return $this->forward_request( 'get_disputes_summary', [ $filters ] ); } /** * Retrieve dispute to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_dispute( $request ) { $dispute_id = $request->get_param( 'dispute_id' ); return $this->forward_request( 'get_dispute', [ $dispute_id ] ); } /** * Update and respond with dispute via API. * * @param WP_REST_Request $request Full data about the request. */ public function update_dispute( $request ) { $params = $request->get_params(); return $this->forward_request( 'update_dispute', [ $params['dispute_id'], $params['evidence'], $params['submit'], $params['metadata'], ] ); } /** * Close and respond with dispute via API. * * @param WP_REST_Request $request Full data about the request. */ public function close_dispute( $request ) { $dispute_id = $request->get_param( 'dispute_id' ); return $this->forward_request( 'close_dispute', [ $dispute_id ] ); } /** * Initiate disputes export via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_disputes_export( $request ) { $user_email = $request->get_param( 'user_email' ); $locale = $request->get_param( 'locale' ); $filters = $this->get_disputes_filters( $request ); return $this->forward_request( 'get_disputes_export', [ $filters, $user_email, $locale ] ); } /** * Extract disputes filters from request * The reason to map the filter properties is to keep consitency between the front-end models. * * @param WP_REST_Request $request Full data about the request. */ private function get_disputes_filters( $request ) { return array_filter( [ 'match' => $request->get_param( 'match' ), 'currency_is' => $request->get_param( 'store_currency_is' ), 'created_before' => $request->get_param( 'date_before' ), 'created_after' => $request->get_param( 'date_after' ), 'created_between' => $request->get_param( 'date_between' ), 'search' => $request->get_param( 'search' ), 'status_is' => $request->get_param( 'status_is' ), 'status_is_not' => $request->get_param( 'status_is_not' ), ], static function ( $filter ) { return null !== $filter; } ); } } includes/admin/class-wc-rest-payments-tos-controller.php 0000777 00000011655 15251156605 0017511 0 ustar 00 <?php /** * Class WC_REST_Payments_Tos_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request\Add_Account_Tos_Agreement; use WCPay\Exceptions\Rest_Request_Exception; use WCPay\Logger; defined( 'ABSPATH' ) || exit; /** * REST controller for Terms of Services routes. */ class WC_REST_Payments_Tos_Controller extends WC_Payments_REST_Controller { /** * Result codes for returning to the WCPay server API. They don't have any special meaning, but can will be logged * and are therefore useful when debugging how we reacted to a webhook. */ const RESULT_SUCCESS = 'success'; const RESULT_BAD_REQUEST = 'bad_request'; const RESULT_ERROR = 'error'; /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/tos'; /** * Instance of WC_Payment_Gateway_WCPay * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * WC Payments Account. * * @var WC_Payments_Account */ private $account; /** * WC_REST_Payments_Webhook_Controller constructor. * * @param WC_Payments_API_Client $api_client WC_Payments_API_Client instance. * @param WC_Payment_Gateway_WCPay $gateway WC_Payment_Gateway_WCPay instance. * @param WC_Payments_Account $account WC_Payments_Account instance. */ public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Account $account ) { parent::__construct( $api_client ); $this->gateway = $gateway; $this->account = $account; } /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'handle_tos' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/reactivate', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'reactivate' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/stripe_track_connected', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'remove_stripe_connect_track' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Record ToS acceptance. * * @param WP_REST_Request $request Full data about the request. * * @return WP_REST_Response * @throws Rest_Request_Exception Throw if accept param is missing. */ public function handle_tos( $request ) { $body = $request->get_json_params(); try { if ( ! isset( $body['accept'] ) ) { throw new Rest_Request_Exception( __( 'ToS accept parameter is missing', 'woocommerce-payments' ) ); } $is_accepted = (bool) $body['accept']; Logger::debug( sprintf( 'ToS acceptance request received. Accept: %s', $is_accepted ? 'yes' : 'no' ) ); if ( $is_accepted ) { $this->handle_tos_accepted(); } else { $this->handle_tos_declined(); } } catch ( Rest_Request_Exception $e ) { Logger::error( $e ); return new WP_REST_Response( [ 'result' => self::RESULT_BAD_REQUEST ], 400 ); } catch ( Exception $e ) { Logger::error( $e ); return new WP_REST_Response( [ 'result' => self::RESULT_ERROR ], 500 ); } return new WP_REST_Response( [ 'result' => self::RESULT_SUCCESS ] ); } /** * Process ToS accepted. */ private function handle_tos_accepted() { $this->gateway->enable(); // Accessing directly, because a user must be already logged in. $current_user = wp_get_current_user(); $user_name = $current_user->user_login; $request = Add_Account_Tos_Agreement::create(); $request->set_source( 'settings-popup' ); $request->set_user_name( $user_name ); $request->send(); $this->account->refresh_account_data(); } /** * Process ToS declined. */ private function handle_tos_declined() { // TODO: maybe record ToS declined data. $this->gateway->disable(); } /** * Activates the gateway again, after it's been disabled. * * @param WP_REST_Request $request Full data about the request. * * @return WP_REST_Response */ public function reactivate( $request ) { try { $this->gateway->enable(); Logger::debug( 'Gateway re-enabled after ToS decline.' ); } catch ( Exception $e ) { Logger::error( $e ); return new WP_REST_Response( [ 'result' => self::RESULT_ERROR ], 500 ); } return new WP_REST_Response( [ 'result' => self::RESULT_SUCCESS ] ); } /** * Deletes _wcpay_onboarding_stripe_connected option after KYC completion has been tracked. * * @param WP_REST_Request $request Full data about the request. * * @return WP_REST_Response */ public function remove_stripe_connect_track( $request ) { delete_option( '_wcpay_onboarding_stripe_connected' ); return new WP_REST_Response( [ 'result' => self::RESULT_SUCCESS ] ); } } includes/admin/class-wc-rest-payments-customer-controller.php 0000777 00000021227 15251156605 0020541 0 ustar 00 <?php /** * Class WC_REST_Payments_Customer_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request; use WCPay\Exceptions\API_Exception; defined( 'ABSPATH' ) || exit; /** * REST controller for customers. */ class WC_REST_Payments_Customer_Controller extends WC_Payments_REST_Controller { /** * Onboarding Service. * * @var WC_Payments_Customer_Service */ protected $customer_service; /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/customers'; /** * Constructor. * * @param WC_Payments_API_Client $api_client WooCommerce Payments API client. * @param WC_Payments_Customer_Service $customer_service Token service. */ public function __construct( WC_Payments_API_Client $api_client, WC_Payments_Customer_Service $customer_service ) { parent::__construct( $api_client ); $this->customer_service = $customer_service; } /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<customer_id>\w+)/payment_methods', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_customer_payment_methods' ], 'permission_callback' => [ $this, 'check_permission' ], ], 'schema' => [ $this, 'get_item_schema' ], ] ); } /** * Retrieve transaction to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_customer_payment_methods( $request ) { $customer_id = $request->get_param( 'customer_id' ); $payment_methods_types = WC_Payments::get_gateway()->get_upe_enabled_payment_method_ids() ?? []; $payment_methods = []; // Perhaps we can fetch it directly from server and avoid looping to get payment methods from cache. foreach ( $payment_methods_types as $type ) { try { $payment_methods[] = $this->customer_service->get_payment_methods_for_customer( $customer_id, $type ); } catch ( API_Exception $e ) { wp_send_json_error( wp_strip_all_tags( $e->getMessage() ), 403 ); } } $payment_methods = array_merge( ...$payment_methods ); $data = []; foreach ( $payment_methods as $payment_method ) { $response = $this->prepare_item_for_response( $payment_method, $request ); $data[] = $this->prepare_response_for_collection( $response ); } return rest_ensure_response( $data ); } /** * Prepare each item for response. * * @param array|mixed $item Item to prepare. * @param WP_REST_Request $request Request instance. * * @return WP_REST_Response|WP_Error */ public function prepare_item_for_response( $item, $request ) { $prepared_item = []; $prepared_item['id'] = $item['id']; $prepared_item['type'] = $item['type']; $prepared_item['billing_details'] = $item['billing_details']; if ( array_key_exists( 'card', $item ) ) { $prepared_item['card'] = [ 'brand' => $item['card']['brand'], 'last4' => $item['card']['last4'], 'exp_month' => $item['card']['exp_month'], 'exp_year' => $item['card']['exp_year'], ]; } if ( array_key_exists( 'card', $item ) ) { $prepared_item['card'] = [ 'brand' => $item['card']['brand'], 'last4' => $item['card']['last4'], 'exp_month' => $item['card']['exp_month'], 'exp_year' => $item['card']['exp_year'], ]; } elseif ( array_key_exists( 'sepa_debit', $item ) ) { $prepared_item['sepa_debit'] = [ 'last4' => $item['sepa_debit']['last4'], ]; } elseif ( array_key_exists( 'link', $item ) ) { $prepared_item['link'] = [ 'email' => $item['link']['email'], ]; } $context = $request['context'] ?? 'view'; $prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request ); $prepared_item = $this->filter_response_by_context( $prepared_item, $context ); return rest_ensure_response( $prepared_item ); } /** * Item schema. * * @return array */ public function get_item_schema() { return [ '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'payment_method', 'type' => 'object', 'properties' => [ 'id' => [ 'description' => __( 'ID for the payment method.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'type' => [ 'description' => __( 'Type of the payment method.', 'woocommerce-payments' ), 'type' => 'string', 'enum' => [ 'card', 'sepa_debit', 'link' ], 'context' => [ 'view' ], ], 'billing_details' => [ 'description' => __( 'Billing details for the payment method.', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'address' => [ 'description' => __( 'Address associated with the billing details.', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'city' => [ 'description' => __( 'City of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'country' => [ 'description' => __( 'Country of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'line1' => [ 'description' => __( 'Line 1 of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'line2' => [ 'description' => __( 'Line 2 of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'postal_code' => [ 'description' => __( 'Postal code of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'state' => [ 'description' => __( 'State of the billing address.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ], 'email' => [ 'description' => __( 'Email associated with the billing details.', 'woocommerce-payments' ), 'type' => 'string', 'format' => 'email', 'context' => [ 'view' ], ], 'name' => [ 'description' => __( 'Name associated with the billing details.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'phone' => [ 'description' => __( 'Phone number associated with the billing details.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ], 'card' => [ 'description' => __( 'Card details for the payment method.', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'brand' => [ 'description' => __( 'Brand of the card.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'last4' => [ 'description' => __( 'Last 4 digits of the card.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'exp_month' => [ 'description' => __( 'Expiration month of the card.', 'woocommerce-payments' ), 'type' => 'integer', 'context' => [ 'view' ], ], 'exp_year' => [ 'description' => __( 'Expiration year of the card.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ], 'sepa_debit' => [ 'description' => __( 'SEPA Debit details for the payment method.', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'last4' => [ 'description' => __( 'Last 4 digits of the SEPA Debit.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ], 'link' => [ 'description' => __( 'Link details for the payment method.', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'email' => [ 'description' => __( 'Email associated with the link.', 'woocommerce-payments' ), 'type' => 'string', 'format' => 'email', 'context' => [ 'view' ], ], ], ], ], ]; } } includes/admin/class-wc-rest-payments-deposits-controller.php 0000777 00000012665 15251156605 0020540 0 ustar 00 <?php /** * Class WC_REST_Payments_Deposits_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request; use WCPay\Core\Server\Request\List_Deposits; defined( 'ABSPATH' ) || exit; /** * REST controller for deposits. */ class WC_REST_Payments_Deposits_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/deposits'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_deposits' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/summary', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_deposits_summary' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/download', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'get_deposits_export' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/download/(?P<export_id>.*)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_export_url' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<deposit_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_deposit' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/overview-all', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_all_deposits_overviews' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'manual_deposit' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve deposits to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_deposits( $request ) { $wcpay_request = List_Deposits::from_rest_request( $request ); return $wcpay_request->handle_rest_request(); } /** * Retrieve deposits summary to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_deposits_summary( $request ) { $filters = $this->get_deposits_filters( $request ); return $this->forward_request( 'get_deposits_summary', [ $filters ] ); } /** * Retrieve an overview of all deposits from the API. */ public function get_all_deposits_overviews() { $request = Request::get( WC_Payments_API_Client::DEPOSITS_API . '/overview-all' ); $request->assign_hook( 'wcpay_get_all_deposits_overviews' ); return $request->handle_rest_request(); } /** * Retrieve deposit to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_deposit( $request ) { $deposit_id = $request->get_param( 'deposit_id' ); $wcpay_request = Request::get( WC_Payments_API_Client::DEPOSITS_API, $deposit_id ); $wcpay_request->assign_hook( 'wcpay_get_deposit' ); return $wcpay_request->handle_rest_request(); } /** * Initiate deposits export via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_deposits_export( $request ) { $user_email = $request->get_param( 'user_email' ); $locale = $request->get_param( 'locale' ); $filters = $this->get_deposits_filters( $request ); return $this->forward_request( 'get_deposits_export', [ $filters, $user_email, $locale ] ); } /** * Get the payouts export URL for a given export ID, if available. * * @param WP_REST_Request $request Full data about the request. */ public function get_export_url( $request ) { $export_id = $request->get_param( 'export_id' ); return $this->forward_request( 'get_payouts_export_url', [ $export_id ] ); } /** * Extract deposits filters from request * * @param WP_REST_Request $request Full data about the request. */ private function get_deposits_filters( $request ) { return array_filter( [ 'match' => $request->get_param( 'match' ), 'store_currency_is' => $request->get_param( 'store_currency_is' ), 'date_before' => $request->get_param( 'date_before' ), 'date_after' => $request->get_param( 'date_after' ), 'date_between' => $request->get_param( 'date_between' ), 'status_is' => $request->get_param( 'status_is' ), 'status_is_not' => $request->get_param( 'status_is_not' ), ], static function ( $filter ) { return null !== $filter; } ); } /** * Trigger a manual deposit. * * @param WP_REST_Request $request Full data about the request. */ public function manual_deposit( $request ) { $params = $request->get_params(); return $this->forward_request( 'manual_deposit', [ $params['type'], $params['currency'] ] ); } } includes/admin/class-wc-rest-payments-timeline-controller.php 0000777 00000002062 15251156605 0020502 0 ustar 00 <?php /** * Class WC_REST_Payments_Timeline_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; /** * REST controller for the timeline, which includes all events related to an intention. */ class WC_REST_Payments_Timeline_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/timeline'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<intention_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_timeline' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve timeline to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_timeline( $request ) { $intention_id = $request->get_param( 'intention_id' ); return $this->forward_request( 'get_timeline', [ $intention_id ] ); } } includes/admin/tasks/class-wc-payments-task-disputes.php 0000777 00000022461 15251156605 0017472 0 ustar 00 <?php /** * Class WC_Payments_Task_Disputes * * @package WooCommerce\Payments\Tasks */ namespace WooCommerce\Payments\Tasks; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task; use WCPay\Database_Cache; use WC_Payments_Utils; use WC_Payments_API_Client; defined( 'ABSPATH' ) || exit; /** * WC Onboarding Task displayed if disputes awaiting response. * * Note: this task is separate to the Payments → Overview disputes task, which is defined in client/overview/task-list/tasks.js. */ class WC_Payments_Task_Disputes extends Task { /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $api_client; /** * Database_Cache instance. * * @var Database_Cache */ private $database_cache; /** * Disputes due within 7 days. * * @var array|null */ private $disputes_due_within_7d; /** * Disputes due within 1 day. * * @var array|null */ private $disputes_due_within_1d; /** * A memory cache of all disputes needing response. * * @var array|null */ private $disputes_needing_response = null; /** * WC_Payments_Task_Disputes constructor. */ public function __construct() { $this->api_client = \WC_Payments::get_payments_api_client(); $this->database_cache = \WC_Payments::get_database_cache(); parent::__construct(); } /** * Initialize the task. */ private function fetch_relevant_disputes() { $this->disputes_due_within_7d = $this->get_disputes_needing_response_within_days( 7 ); $this->disputes_due_within_1d = $this->get_disputes_needing_response_within_days( 1 ); } /** * Gets the task ID. * * @return string */ public function get_id() { return 'woocommerce_payments_disputes_task'; } /** * Gets the task title. * * @return string */ public function get_title() { if ( null === $this->disputes_needing_response ) { $this->fetch_relevant_disputes(); } if ( count( (array) $this->disputes_due_within_7d ) === 1 ) { $dispute = $this->disputes_due_within_7d[0]; $amount = WC_Payments_Utils::interpret_stripe_amount( $dispute['amount'], $dispute['currency'] ); $amount_formatted = WC_Payments_Utils::format_currency( $amount, $dispute['currency'] ); if ( count( (array) $this->disputes_due_within_1d ) > 0 ) { return sprintf( /* translators: %s is a currency formatted amount */ __( 'Respond to a dispute for %s – Last day', 'woocommerce-payments' ), $amount_formatted ); } return sprintf( /* translators: %s is a currency formatted amount */ __( 'Respond to a dispute for %s', 'woocommerce-payments' ), $amount_formatted ); } $active_disputes = $this->get_disputes_needing_response(); if ( ! is_array( $active_disputes ) || count( $active_disputes ) === 0 ) { return ''; } $dispute_currencies = array_unique( array_column( $active_disputes, 'currency' ) ); // If multiple currencies, use simple task title without total amounts. if ( count( $dispute_currencies ) > 1 ) { return sprintf( // translators: %d is a number greater than 1. __( 'Respond to %d active disputes', 'woocommerce-payments' ), count( $active_disputes ) ); } // If single currency, calculate total amount and include in task title. $dispute_total = array_reduce( $active_disputes, function ( $total, $dispute ) { return $total + ( $dispute['amount'] ?? 0 ); }, 0 ); $dispute_total_formatted = WC_Payments_Utils::format_currency( WC_Payments_Utils::interpret_stripe_amount( $dispute_total, $dispute_currencies[0] ), $dispute_currencies[0] ); return sprintf( /* translators: %d is a number greater than 1. %s is a formatted amount, eg: $10.00 */ __( 'Respond to %1$d active disputes for a total of %2$s', 'woocommerce-payments' ), count( $active_disputes ), $dispute_total_formatted ); } /** * Get the parent list ID. * * This function prior to WC 6.4.0 was abstract and so is needed for backwards compatibility. * * @return string */ public function get_parent_id() { // WC 6.4.0 compatibility. if ( is_callable( [ parent::class, 'get_parent_id' ] ) ) { return parent::get_parent_id(); } return 'extended'; } /** * Gets the task subtitle. * * @return string */ public function get_additional_info() { if ( count( (array) $this->disputes_due_within_7d ) === 1 ) { $local_timezone = new \DateTimeZone( wp_timezone_string() ); $dispute = $this->disputes_due_within_7d[0]; $due_by_local_time = ( new \DateTime( $dispute['due_by'] ) )->setTimezone( $local_timezone ); // Sum of Unix timestamp and timezone offset in seconds. $due_by_ts = $due_by_local_time->getTimestamp() + $due_by_local_time->getOffset(); if ( count( (array) $this->disputes_due_within_1d ) > 0 ) { return sprintf( /* translators: %s is time, eg: 11:59 PM */ __( 'Respond today by %s', 'woocommerce-payments' ), date_i18n( wc_time_format(), $due_by_ts ) ); } $now = new \DateTime( 'now', $local_timezone ); $diff = $now->diff( $due_by_local_time ); return sprintf( /* translators: %1$s is a date, eg: Jan 1, 2021. %2$s is the number of days left, eg: 2 days. */ __( 'By %1$s – %2$s left to respond', 'woocommerce-payments' ), date_i18n( wc_date_format(), $due_by_ts ), /* translators: %s is the number of days left, e.g. 1 day. */ sprintf( _n( '%d day', '%d days', $diff->days, 'woocommerce-payments' ), $diff->days ) ); } if ( count( (array) $this->disputes_due_within_1d ) > 0 ) { return sprintf( /* translators: %d is the number of disputes. */ __( 'Final day to respond to %d of the disputes', 'woocommerce-payments' ), count( (array) $this->disputes_due_within_1d ) ); } return sprintf( /* translators: %d is the number of disputes. */ __( 'Last week to respond to %d of the disputes', 'woocommerce-payments' ), count( (array) $this->disputes_due_within_7d ) ); } /** * Gets the task's action URL. * * @return string */ public function get_action_url() { $disputes = $this->disputes_due_within_7d; if ( count( (array) $disputes ) === 1 ) { $dispute = $disputes[0]; return admin_url( add_query_arg( [ 'page' => 'wc-admin', 'path' => '%2Fpayments%2Ftransactions%2Fdetails', 'id' => $dispute['charge_id'], ], 'admin.php' ) ); } return admin_url( add_query_arg( [ 'page' => 'wc-admin', 'path' => '%2Fpayments%2Fdisputes', 'filter' => 'awaiting_response', ], 'admin.php' ) ); } /** * Get the estimated time to complete the task. * * @return string */ public function get_time() { return ''; } /** * Gets the task content. * * @return string */ public function get_content() { return ''; } /** * Get whether the task is completed. * * @return bool */ public function is_complete() { return false; } /** * Get whether the task is visible. * * @return bool */ public function can_view() { if ( null === $this->disputes_needing_response ) { $this->fetch_relevant_disputes(); } return count( (array) $this->disputes_due_within_7d ) > 0; } /** * Get disputes needing response within the given number of days. * * @param int $num_days Number of days in the future to check for disputes needing response. * * @return array Disputes needing response within the given number of days. */ private function get_disputes_needing_response_within_days( $num_days ) { $to_return = []; $active_disputes = $this->get_disputes_needing_response(); if ( ! is_array( $active_disputes ) ) { return $to_return; } foreach ( $active_disputes as $dispute ) { if ( ! $dispute['due_by'] ) { continue; } // Compare UTC times. $now_utc = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) ); $due_by_utc = new \DateTime( $dispute['due_by'], new \DateTimeZone( 'UTC' ) ); if ( $now_utc > $due_by_utc ) { continue; } $diff = $now_utc->diff( $due_by_utc ); // If the dispute is due within the given number of days, add it to the list. if ( $diff->days <= $num_days ) { $to_return[] = $dispute; } } return $to_return; } /** * Gets disputes awaiting a response. ie have a 'needs_response' or 'warning_needs_response' status. * * @return array|null Array of disputes awaiting a response. Null on failure. */ private function get_disputes_needing_response() { if ( null !== $this->disputes_needing_response ) { return $this->disputes_needing_response; } $this->disputes_needing_response = $this->database_cache->get_or_add( Database_Cache::ACTIVE_DISPUTES_KEY, function () { try { $response = $this->api_client->get_disputes( [ 'pagesize' => 50, 'search' => [ 'warning_needs_response', 'needs_response' ], ] ); } catch ( \Exception $e ) { // Ensure an array is always returned, even if the API call fails. return []; } $active_disputes = $response['data'] ?? []; // sort by due_by date ascending. usort( $active_disputes, function ( $a, $b ) { $a_due_by = new \DateTime( $a['due_by'] ); $b_due_by = new \DateTime( $b['due_by'] ); return $a_due_by <=> $b_due_by; } ); return $active_disputes; }, 'is_array' ); return $this->disputes_needing_response; } } includes/admin/class-wc-payments-rest-controller.php 0000777 00000002722 15251156605 0016701 0 ustar 00 <?php /** * Class WC_Payments_REST_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Exceptions\API_Exception; defined( 'ABSPATH' ) || exit; /** * REST controller for transactions. */ class WC_Payments_REST_Controller extends WP_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ protected $api_client; /** * WC_Payments_REST_Controller constructor. * * @param WC_Payments_API_Client $api_client - WooCommerce Payments API client. */ public function __construct( WC_Payments_API_Client $api_client ) { $this->api_client = $api_client; } /** * Forwards request to API client with taking care of API_Exception. * * @param string $api_method - API method name. * @param array $args - API method args. * * @return WP_Error|mixed - Method result of WP_Error in case of API_Exception. */ public function forward_request( $api_method, $args ) { try { $response = call_user_func_array( [ $this->api_client, $api_method ], $args ); } catch ( API_Exception $e ) { $response = new WP_Error( $e->get_error_code(), $e->getMessage() ); } return rest_ensure_response( $response ); } /** * Verify access. * * Override this method if custom permissions required. */ public function check_permission() { return current_user_can( 'manage_woocommerce' ); } } includes/admin/class-wc-rest-payments-capital-controller.php 0000777 00000003131 15251156605 0020307 0 ustar 00 <?php /** * Class WC_REST_Payments_Capital_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request; defined( 'ABSPATH' ) || exit; /** * REST controller for Capital loans functionality. */ class WC_REST_Payments_Capital_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/capital'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/active_loan_summary', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_active_loan_summary' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/loans', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_loans' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve the summary of the active Capital loan. */ public function get_active_loan_summary() { $request = Request::get( WC_Payments_API_Client::CAPITAL_API . '/active_loan_summary' ); $request->assign_hook( 'wcpay_get_active_loan_summary_request' ); return $request->handle_rest_request(); } /** * Retrieve all the past and present Capital loans. */ public function get_loans() { $request = Request::get( WC_Payments_API_Client::CAPITAL_API . '/loans' ); $request->assign_hook( 'wcpay_get_loans_request' ); return $request->handle_rest_request(); } } includes/admin/class-wc-rest-payments-refunds-controller.php 0000777 00000006524 15251156605 0020351 0 ustar 00 <?php /** * Class WC_REST_Payments_Timeline_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Core\Server\Request\Refund_Charge; use WCPay\Exceptions\API_Exception; defined( 'ABSPATH' ) || exit; /** * REST controller for the timeline, which includes all events related to an intention. */ class WC_REST_Payments_Refunds_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/refund'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'process_refund' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Makes direct refund bypassing any order checks. * * @internal Not intended for usage in integrations or outside of WooCommerce Payments. * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response|WP_Error */ public function process_refund( $request ) { $order_id = $request->get_param( 'order_id' ); $charge_id = $request->get_param( 'charge_id' ); $amount = $request->get_param( 'amount' ); $reason = $request->get_param( 'reason' ); $order = null; if ( $order_id ) { $order = wc_get_order( $order_id ); if ( false !== $order && $order instanceof WC_Order ) { $result = $this->process_order_refund( $order, $amount, $reason ); if ( is_wp_error( $result ) || false === $result ) { return rest_ensure_response( new WP_Error( 'wcpay_refund_payment', __( 'Failed to create refund', 'woocommerce-payments' ) ) ); } return rest_ensure_response( $result ); } } try { return rest_ensure_response( $this->process_charge_refund( $charge_id, $amount, $reason ) ); } catch ( API_Exception $e ) { if ( 'insufficient_balance_for_refund' === $e->get_error_code() && $order instanceof WC_Order ) { WC_Payments::get_order_service()->handle_insufficient_balance_for_refund( $order, $amount ); } return rest_ensure_response( new WP_Error( 'wcpay_refund_payment', $e->getMessage() ) ); } } /** * Process refund for an order. * * @param WC_Order $order The order to refund. * @param int $amount Refund amount. * @param string $reason Refund reason. * @return WC_Order_Refund|WP_Error|false */ private function process_order_refund( WC_Order $order, $amount, $reason ) { return wc_create_refund( [ 'amount' => WC_Payments_Utils::interpret_stripe_amount( $amount, $order->get_currency() ), 'reason' => $reason, 'order_id' => $order->get_id(), 'refund_payment' => true, 'restock_items' => true, ] ); } /** * Process refund for a charge. * * @param string $charge_id The charge to refund. * @param int $amount Refund amount. * @param string $reason Refund reason. * @return array */ private function process_charge_refund( $charge_id, $amount, $reason ) { $refund_request = Refund_Charge::create( $charge_id ); $refund_request->set_charge( $charge_id ); $refund_request->set_amount( $amount ); $refund_request->set_reason( $reason ); $refund_request->set_source( 'transaction_details_no_order' ); return $refund_request->send(); } } includes/admin/class-wc-rest-payments-accounts-controller.php 0000777 00000005215 15251156605 0020516 0 ustar 00 <?php /** * Class WC_REST_Payments_Accounts_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; /** * REST controller for account details and status. */ class WC_REST_Payments_Accounts_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/accounts'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/payments/accounts', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_account_data' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/session', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'create_embedded_account_session' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Get account details via API. * * @return WP_REST_Response|WP_Error */ public function get_account_data() { $account = WC_Payments::get_account_service()->get_cached_account_data(); if ( [] === $account ) { $default_currency = get_woocommerce_currency(); $status = WC_Payments_Account::is_on_boarding_disabled() ? 'ONBOARDING_DISABLED' : 'NOACCOUNT'; $account = [ 'card_present_eligible' => false, 'country' => WC()->countries->get_base_country(), 'current_deadline' => null, 'has_overdue_requirements' => false, 'has_pending_requirements' => false, 'statement_descriptor' => '', 'status' => $status, 'store_currencies' => [ 'default' => $default_currency, 'supported' => [ $default_currency, ], ], 'customer_currencies' => [ 'supported' => [ $default_currency, ], ], ]; } if ( false !== $account ) { // Add extra properties to account if necessary. $account['card_present_eligible'] = false; $account['test_mode'] = WC_Payments::mode()->is_test(); $account['test_mode_onboarding'] = WC_Payments::mode()->is_test_mode_onboarding(); } return rest_ensure_response( $account ); } /** * Create an account embedded session via the API. * * @return WP_Error|WP_REST_Response */ public function create_embedded_account_session() { $account_session = WC_Payments::get_account_service()->create_embedded_account_session(); if ( $account_session ) { $account_session['locale'] = get_user_locale(); } return rest_ensure_response( $account_session ); } } includes/admin/class-wc-rest-payments-files-controller.php 0000777 00000012511 15251156605 0017776 0 ustar 00 <?php /** * Class WC_REST_Payments_Files_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; /** * REST controller for files. */ class WC_REST_Payments_Files_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/file'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'upload_file' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<file_id>\w+)/details', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_file_detail' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<file_id>\w+)/content', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_file_content' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<file_id>\w+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_file' ], 'permission_callback' => [], ] ); } /** * Create file and respond with file object via API. * * @param WP_REST_Request $request Full data about the request. */ public function upload_file( $request ) { return $this->forward_request( 'upload_file', [ $request ] ); } /** * Retrieve a file content via API. * * @param WP_REST_Request $request - request object. * * @return WP_Error|WP_HTTP_Response */ public function get_file( WP_REST_Request $request ) { $file_id = $request->get_param( 'file_id' ); $as_account = (bool) $request->get_param( 'as_account' ); $file_service = new WC_Payments_File_Service(); $purpose = get_transient( WC_Payments_File_Service::CACHE_KEY_PREFIX_PURPOSE . $file_id . '_' . ( $as_account ? '1' : '0' ) ); if ( ! $purpose ) { $file = $this->forward_request( 'get_file', [ $file_id, $as_account ] ); if ( is_wp_error( $file ) ) { return $this->file_error_response( $file ); } $purpose = $file->get_data()['purpose']; set_transient( WC_Payments_File_Service::CACHE_KEY_PREFIX_PURPOSE . $file_id, $purpose, WC_Payments_File_Service::CACHE_PERIOD ); } if ( ! $file_service->is_file_public( $purpose ) && ! $this->check_permission() ) { return new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to do that.', 'woocommerce-payments' ), [ 'status' => rest_authorization_required_code() ] ); } $result = $this->forward_request( 'get_file_contents', [ $file_id, $as_account ] ); if ( is_wp_error( $result ) ) { return $this->file_error_response( $result ); } /** * WP_REST_Server will convert the response data to JSON prior to output it. * Using this filter to prevent it, and output the data from WP_HTTP_Response instead. */ add_filter( 'rest_pre_serve_request', function ( bool $served, WP_HTTP_Response $response ): bool { echo $response->get_data(); // @codingStandardsIgnoreLine return true; }, 10, 2 ); return new WP_HTTP_Response( base64_decode( $result->get_data()['file_content'] ), // @codingStandardsIgnoreLine 200, [ 'Content-Type' => $result->get_data()['content_type'], 'Content-Disposition' => 'inline', ] ); } /** * Retrieve file details via the API. * * Example response: * { * "id": "file_1Np1S5J5cIRIG92xknlr0iND", * "object": "file", * "created": 1694405421, * "expires_at": 1717733421, * "filename": "Screenshot 2023-09-04 at 5.08.31\u202fPM.png", * "purpose": "dispute_evidence", * "size": 21444, * "title": null, * "type": "png", * } * * @param WP_REST_Request $request Full data about the request. * * @return mixed|WP_Error */ public function get_file_detail( WP_REST_Request $request ) { $file_id = $request->get_param( 'file_id' ); $as_account = (bool) $request->get_param( 'as_account' ); return $this->forward_request( 'get_file', [ $file_id, $as_account ] ); } /** * Retrieve file contents via the API as a base64 encoded string. * * Example response: * { * "content_type": "image\/png", * "file_content": "iVBORw.......", * } * * @param WP_REST_Request $request Full data about the request. * * @return mixed|WP_Error */ public function get_file_content( WP_REST_Request $request ) { $file_id = $request->get_param( 'file_id' ); $as_account = (bool) $request->get_param( 'as_account' ); return $this->forward_request( 'get_file_contents', [ $file_id, $as_account ] ); } /** * Convert error response * * @param WP_Error $error - error. * * @return WP_Error */ private function file_error_response( WP_Error $error ): WP_Error { $error_status_code = 'resource_missing' === $error->get_error_code() ? WP_Http::NOT_FOUND : WP_Http::INTERNAL_SERVER_ERROR; return new WP_Error( $error->get_error_code(), $error->get_error_message(), [ 'status' => $error_status_code ] ); } } includes/admin/class-wc-rest-payments-pm-promotions-controller.php 0000777 00000010147 15251156605 0021522 0 ustar 00 <?php /** * Class WC_REST_Payments_Promotions_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; /** * REST controller for payment methods (PM) promotions functionality. */ class WC_REST_Payments_PM_Promotions_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/pm-promotions'; /** * The PM promotions service. * * @var WC_Payments_PM_Promotions_Service */ private $promotions_service; /** * WC_REST_Payments_PM_Promotions_Controller constructor. * * @param WC_Payments_API_Client $api_client WooPayments API client. * @param WC_Payments_PM_Promotions_Service $promotions_service The PM promotions service. */ public function __construct( WC_Payments_API_Client $api_client, WC_Payments_PM_Promotions_Service $promotions_service ) { parent::__construct( $api_client ); $this->promotions_service = $promotions_service; } /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_promotions' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)/activate', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'activate_promotion' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'string', 'description' => __( 'The promotion unique identifier.', 'woocommerce-payments' ), 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => [ $this, 'validate_promotion_id' ], ], ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)/dismiss', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'dismiss_promotion' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'string', 'description' => __( 'The promotion unique identifier.', 'woocommerce-payments' ), 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => [ $this, 'validate_promotion_id' ], ], ], ] ); } /** * Retrieve the active promotions list. * * @return WP_REST_Response|WP_Error */ public function get_promotions() { $promotions = $this->promotions_service->get_visible_promotions(); return rest_ensure_response( $promotions ?? [] ); } /** * Activate a promotion. * * @param WP_REST_Request $request Full data about the request. * * @return WP_REST_Response|WP_Error */ public function activate_promotion( WP_REST_Request $request ) { $result = $this->promotions_service->activate_promotion( $request->get_param( 'id' ) ); return rest_ensure_response( [ 'success' => $result ] ); } /** * Dismiss a promotion. * * @param WP_REST_Request $request Full data about the request. * * @return WP_REST_Response|WP_Error */ public function dismiss_promotion( WP_REST_Request $request ) { $result = $this->promotions_service->dismiss_promotion( $request->get_param( 'id' ) ); return rest_ensure_response( [ 'success' => $result ] ); } /** * Validate the promotion ID parameter. * * @param mixed $value The parameter value. * @param WP_REST_Request $request The request object. * @param string $param The parameter name. * * @return bool True if valid, false otherwise. */ public function validate_promotion_id( $value, WP_REST_Request $request, string $param ): bool { if ( ! is_string( $value ) || empty( $value ) ) { return false; } // Match alphanumeric characters, underscores, and hyphens only. return (bool) preg_match( '/^[a-zA-Z0-9_-]+$/', $value ); } } includes/admin/class-wc-rest-payments-fraud-outcomes-controller.php 0000777 00000002020 15251156605 0021623 0 ustar 00 <?php /** * Class WC_REST_Payments_Orders_Controller * * @package WooCommerce\Payments\Admin */ defined( 'ABSPATH' ) || exit; /** * REST controller for order processing. */ class WC_REST_Payments_Fraud_Outcomes_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/fraud_outcomes'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>\w+)/latest', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_latest_fraud_outcome' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve charge to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_latest_fraud_outcome( $request ) { $id = $request->get_param( 'id' ); return $this->forward_request( 'get_latest_fraud_outcome', [ $id ] ); } } includes/admin/class-wc-rest-payments-webhook-controller.php 0000777 00000004620 15251156605 0020334 0 ustar 00 <?php /** * Class WC_REST_Payments_Webhook_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Exceptions\Invalid_Webhook_Data_Exception; use WCPay\Logger; defined( 'ABSPATH' ) || exit; /** * REST controller for webhooks. */ class WC_REST_Payments_Webhook_Controller extends WC_Payments_REST_Controller { /** * Result codes for returning to the WCPay server API. They don't have any special meaning, but can will be logged * and are therefore useful when debugging how we reacted to a webhook. */ const RESULT_SUCCESS = 'success'; const RESULT_BAD_REQUEST = 'bad_request'; const RESULT_ERROR = 'error'; /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/webhook'; /** * Webhook Processing Service. * * @var WC_Payments_Webhook_Processing_Service */ private $webhook_processing_service; /** * WC_REST_Payments_Webhook_Controller constructor. * * @param WC_Payments_API_Client $api_client WC_Payments_API_Client instance. * @param WC_Payments_Webhook_Processing_Service $webhook_processing_service WC_Payments_Webhook_Processing_Service instance. */ public function __construct( WC_Payments_API_Client $api_client, WC_Payments_Webhook_Processing_Service $webhook_processing_service ) { parent::__construct( $api_client ); $this->webhook_processing_service = $webhook_processing_service; } /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'handle_webhook' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve transactions to respond with via API. * * @param WP_REST_Request $request Full data about the request. * * @return WP_REST_Response */ public function handle_webhook( $request ) { $body = $request->get_json_params(); try { $this->webhook_processing_service->process( $body ); } catch ( Invalid_Webhook_Data_Exception $e ) { Logger::error( $e ); return new WP_REST_Response( [ 'result' => self::RESULT_BAD_REQUEST ], 400 ); } catch ( Exception $e ) { Logger::error( $e ); return new WP_REST_Response( [ 'result' => self::RESULT_ERROR ], 500 ); } return new WP_REST_Response( [ 'result' => self::RESULT_SUCCESS ] ); } } includes/admin/class-wc-rest-payments-settings-controller.php 0000777 00000145527 15251156605 0020552 0 ustar 00 <?php /** * Class WC_REST_Payments_Settings_Controller * * @package WooCommerce\Payments\Admin */ use WCPay\Constants\Payment_Method; use WCPay\Constants\Country_Code; use WCPay\Fraud_Prevention\Fraud_Risk_Tools; use WCPay\Constants\Track_Events; use WCPay\Fraud_Prevention\Models\Rule; use WCPay\Logger; defined( 'ABSPATH' ) || exit; /** * REST controller for settings. */ class WC_REST_Payments_Settings_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/settings'; /** * Instance of WC_Payment_Gateway_WCPay. * * @var WC_Payment_Gateway_WCPay */ private $wcpay_gateway; /** * WC_Payments_Account instance to get information about the account * * @var WC_Payments_Account */ protected $account; /** * WC_Payments_PM_Promotions_Service instance for payment method promotions. * * @var WC_Payments_PM_Promotions_Service */ private $pm_promotions_service; /** * WC_REST_Payments_Settings_Controller constructor. * * @param WC_Payments_API_Client $api_client WC_Payments_API_Client instance. * @param WC_Payment_Gateway_WCPay $wcpay_gateway WC_Payment_Gateway_WCPay instance. * @param WC_Payments_Account $account Account class instance. * @param WC_Payments_PM_Promotions_Service $pm_promotions_service PM Promotions Service instance. */ public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $wcpay_gateway, WC_Payments_Account $account, WC_Payments_PM_Promotions_Service $pm_promotions_service ) { parent::__construct( $api_client ); $this->wcpay_gateway = $wcpay_gateway; $this->account = $account; $this->pm_promotions_service = $pm_promotions_service; } /** * Configure REST API routes. */ public function register_routes() { $wcpay_form_fields = $this->wcpay_gateway->get_form_fields(); register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_settings' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base, [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_settings' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'is_wcpay_enabled' => [ 'description' => sprintf( /* translators: %s: WooPayments */ __( 'If %s should be enabled.', 'woocommerce-payments' ), 'WooPayments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'enabled_payment_method_ids' => [ 'description' => __( 'Payment method IDs that should be enabled. Other methods will be disabled.', 'woocommerce-payments' ), 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => $this->wcpay_gateway->get_upe_available_payment_methods(), ], 'validate_callback' => 'rest_validate_request_arg', ], 'is_manual_capture_enabled' => [ 'description' => sprintf( /* translators: %s: WooPayments */ __( 'If %s manual capture of charges should be enabled.', 'woocommerce-payments' ), 'WooPayments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'is_saved_cards_enabled' => [ 'description' => sprintf( /* translators: %s: WooPayments */ __( 'If %s "Saved cards" should be enabled.', 'woocommerce-payments' ), 'WooPayments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'is_test_mode_enabled' => [ 'description' => sprintf( /* translators: %s: WooPayments */ __( '%s test mode setting.', 'woocommerce-payments' ), 'WooPayments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'is_multi_currency_enabled' => [ 'description' => sprintf( /* translators: %s: WooPayments */ __( '%s Multi-Currency feature flag setting.', 'woocommerce-payments' ), 'WooPayments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'is_wcpay_subscriptions_enabled' => [ 'description' => sprintf( /* translators: %s: WooPayments */ __( '%s Subscriptions feature flag setting.', 'woocommerce-payments' ), 'WooPayments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'account_statement_descriptor' => [ 'description' => sprintf( /* translators: %s: WooPayments */ __( '%s bank account descriptor to be displayed in customers\' bank accounts.', 'woocommerce-payments' ), 'WooPayments' ), 'type' => 'string', 'validate_callback' => [ $this, 'validate_statement_descriptor' ], ], 'account_business_name' => [ 'description' => __( 'The customer-facing business name.', 'woocommerce-payments' ), 'type' => 'string', ], 'account_business_url' => [ 'description' => __( 'The business’s publicly available website.', 'woocommerce-payments' ), 'type' => 'string', ], 'account_business_support_address' => [ 'description' => __( 'A publicly available mailing address for sending support issues to.', 'woocommerce-payments' ), 'type' => 'object', 'validate_callback' => [ $this, 'validate_business_support_address' ], ], 'account_business_support_email' => [ 'description' => __( 'A publicly available email address for sending support issues to.', 'woocommerce-payments' ), 'type' => 'string', 'validate_callback' => [ $this, 'validate_business_support_email_address' ], ], 'account_business_support_phone' => [ 'description' => __( 'A publicly available phone number to call with support issues.', 'woocommerce-payments' ), 'type' => 'string', 'validate_callback' => [ $this, 'validate_business_support_phone' ], ], 'account_branding_logo' => [ 'description' => __( 'A logo id for the account that will be used in Checkout', 'woocommerce-payments' ), 'type' => 'string', ], 'account_branding_icon' => [ 'description' => __( 'An icon for the account.', 'woocommerce-payments' ), 'type' => 'string', ], 'account_branding_primary_color' => [ 'description' => __( 'A CSS hex color value representing the primary branding color for this account.', 'woocommerce-payments' ), 'type' => 'string', ], 'account_branding_secondary_color' => [ 'description' => __( 'A CSS hex color value representing the secondary branding color for this account.', 'woocommerce-payments' ), 'type' => 'string', ], 'account_communications_email' => [ 'description' => __( 'Email address used for WooPayments communications.', 'woocommerce-payments' ), 'type' => 'string', 'validate_callback' => [ $this, 'validate_account_communications_email' ], ], 'deposit_schedule_interval' => [ 'description' => __( 'An interval for deposit scheduling.', 'woocommerce-payments' ), 'type' => 'string', ], 'deposit_schedule_weekly_anchor' => [ 'description' => __( 'Weekly anchor for deposit scheduling when interval is set to weekly', 'woocommerce-payments' ), 'type' => 'string', ], 'deposit_schedule_monthly_anchor' => [ 'description' => __( 'Monthly anchor for deposit scheduling when interval is set to monthly', 'woocommerce-payments' ), 'type' => [ 'integer', 'null' ], ], 'is_payment_request_enabled' => [ 'description' => sprintf( /* translators: %s: WooPayments */ __( 'If %s express checkouts should be enabled.', 'woocommerce-payments' ), 'WooPayments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'is_apple_google_pay_in_payment_methods_options_enabled' => [ 'description' => __( 'If Apple Pay / Google Pay should be enabled as an option in the payment methods list.', 'woocommerce-payments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'payment_request_button_type' => [ 'description' => __( '1-click checkout button types.', 'woocommerce-payments' ), 'type' => 'string', 'items' => [ 'type' => 'string', 'enum' => array_keys( $wcpay_form_fields['payment_request_button_type']['options'] ), ], 'validate_callback' => 'rest_validate_request_arg', ], 'payment_request_button_size' => [ 'description' => __( '1-click checkout button sizes.', 'woocommerce-payments' ), 'type' => 'string', 'items' => [ 'type' => 'string', // it can happen that `$wcpay_form_fields['payment_request_button_size']` is empty (in tests) - fixing temporarily. 'enum' => array_keys( isset( $wcpay_form_fields['payment_request_button_size']['options'] ) ? $wcpay_form_fields['payment_request_button_size']['options'] : [] ), ], 'validate_callback' => 'rest_validate_request_arg', ], 'payment_request_button_theme' => [ 'description' => __( '1-click checkout button themes.', 'woocommerce-payments' ), 'type' => 'string', 'items' => [ 'type' => 'string', 'enum' => array_keys( $wcpay_form_fields['payment_request_button_theme']['options'] ), ], 'validate_callback' => 'rest_validate_request_arg', ], 'payment_request_button_border_radius' => [ 'description' => __( '1-click checkout button border radius.', 'woocommerce-payments' ), 'type' => 'integer', 'validate_callback' => 'rest_validate_request_arg', ], 'is_woopay_enabled' => [ 'description' => __( 'If WooPay should be enabled.', 'woocommerce-payments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'is_amazon_pay_enabled' => [ 'description' => __( 'If Amazon Pay should be enabled.', 'woocommerce-payments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'woopay_custom_message' => [ 'description' => __( 'Custom message to display to WooPay customers.', 'woocommerce-payments' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ], 'woopay_store_logo' => [ 'description' => __( 'Store logo to display to WooPay customers.', 'woocommerce-payments' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ], 'is_stripe_billing_enabled' => [ 'description' => __( 'If Stripe Billing is enabled.', 'woocommerce-payments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'is_migrating_stripe_billing' => [ 'description' => __( 'Whether there is a Stripe Billing off-site to on-site billing migration in progress.', 'woocommerce-payments' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], 'stripe_billing_subscription_count' => [ 'description' => __( 'The number of subscriptions using Stripe Billing', 'woocommerce-payments' ), 'type' => 'integer', 'validate_callback' => 'rest_validate_request_arg', ], 'stripe_billing_migrated_count' => [ 'description' => __( 'The number of subscriptions migrated from Stripe Billing to on-site billing.', 'woocommerce-payments' ), 'type' => 'integer', 'validate_callback' => 'rest_validate_request_arg', ], 'express_checkout_product_methods' => [ 'description' => __( 'Express checkout methods enabled on product page.', 'woocommerce-payments' ), 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => array_keys( $wcpay_form_fields['express_checkout_product_methods']['options'] ?? [] ), ], 'validate_callback' => 'rest_validate_request_arg', ], 'express_checkout_cart_methods' => [ 'description' => __( 'Express checkout methods enabled on cart page.', 'woocommerce-payments' ), 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => array_keys( $wcpay_form_fields['express_checkout_cart_methods']['options'] ?? [] ), ], 'validate_callback' => 'rest_validate_request_arg', ], 'express_checkout_checkout_methods' => [ 'description' => __( 'Express checkout methods enabled on checkout page.', 'woocommerce-payments' ), 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => array_keys( $wcpay_form_fields['express_checkout_checkout_methods']['options'] ?? [] ), ], 'validate_callback' => 'rest_validate_request_arg', ], ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/schedule-stripe-billing-migration', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'schedule_stripe_billing_migration' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Validate the statement descriptor argument. * * @since 4.7.0 * * @param mixed $value The value being validated. * @param WP_REST_Request $request The request made. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ public function validate_statement_descriptor( $value, $request, $param ) { $string_validation_result = rest_validate_request_arg( $value, $request, $param ); if ( true !== $string_validation_result ) { return $string_validation_result; } try { $this->wcpay_gateway->validate_account_statement_descriptor_field( 'account_statement_descriptor', $value ); } catch ( Exception $exception ) { return new WP_Error( 'rest_invalid_pattern', $exception->getMessage() ); } return true; } /** * Validate the business support email. * * @param string $value The value being validated. * @param WP_REST_Request $request The request made. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ public function validate_business_support_email_address( string $value, WP_REST_Request $request, string $param ) { $string_validation_result = rest_validate_request_arg( $value, $request, $param ); if ( true !== $string_validation_result ) { return $string_validation_result; } if ( '' !== $value && ! is_email( $value ) ) { return new WP_Error( 'rest_invalid_pattern', __( 'Error: Invalid email address: ', 'woocommerce-payments' ) . $value ); } return true; } /** * Validate the business support phone. * * @param string $value The value being validated. * @param WP_REST_Request $request The request made. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ public function validate_business_support_phone( string $value, WP_REST_Request $request, string $param ) { $string_validation_result = rest_validate_request_arg( $value, $request, $param ); if ( true !== $string_validation_result ) { return $string_validation_result; } if ( '' !== $value && ! WC_Validation::is_phone( $value ) ) { return new WP_Error( 'rest_invalid_pattern', __( 'Error: Invalid phone number: ', 'woocommerce-payments' ) . $value ); } // Japan accounts require Japanese phone numbers. if ( Country_Code::JAPAN === $this->account->get_account_country() ) { if ( '+81' !== substr( $value, 0, 3 ) ) { return new WP_Error( 'rest_invalid_pattern', __( 'Error: Invalid Japanese phone number: ', 'woocommerce-payments' ) . $value ); } } return true; } /** * Validate the business support address. * * @param array $value The value being validated. * @param WP_REST_Request $request The request made. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ public function validate_business_support_address( array $value, WP_REST_Request $request, string $param ) { $string_validation_result = rest_validate_request_arg( $value, $request, $param ); if ( true !== $string_validation_result ) { return $string_validation_result; } if ( [] !== $value ) { foreach ( $value as $field => $field_value ) { if ( ! in_array( $field, [ 'city', 'country', 'line1', 'line2', 'postal_code', 'state' ], true ) ) { return new WP_Error( 'rest_invalid_pattern', __( 'Error: Invalid address format!', 'woocommerce-payments' ) ); } } } return true; } /** * Validate the account communications email. * * @param string $value The value being validated. * @param WP_REST_Request $request The request made. * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ public function validate_account_communications_email( string $value, WP_REST_Request $request, string $param ) { $string_validation_result = rest_validate_request_arg( $value, $request, $param ); if ( true !== $string_validation_result ) { return $string_validation_result; } if ( '' === $value ) { return new WP_Error( 'rest_invalid_pattern', __( 'Error: Communications email is required.', 'woocommerce-payments' ) ); } if ( ! is_email( $value ) ) { return new WP_Error( 'rest_invalid_pattern', __( 'Error: Invalid email address: ', 'woocommerce-payments' ) . $value ); } return true; } /** * Retrieve settings. * * @return WP_REST_Response */ public function get_settings(): WP_REST_Response { $wcpay_form_fields = $this->wcpay_gateway->get_form_fields(); $available_upe_payment_methods = $this->wcpay_gateway->get_upe_available_payment_methods(); /** * It might be possible that enabled payment methods settings have an invalid state. As an example, * if an account is switched to a new country and earlier country had PM's that are no longer valid; or if the PM is not available anymore. * To keep saving settings working, we are ensuring the enabled payment methods are yet available. */ $enabled_payment_methods = array_values( array_intersect( $this->wcpay_gateway->get_upe_enabled_payment_method_ids(), $available_upe_payment_methods ) ); // Gather the status of the Stripe Billing migration for use on the settings page. if ( class_exists( 'WC_Subscriptions' ) ) { $stripe_billing_migrated_count = $this->wcpay_gateway->get_subscription_migrated_count(); if ( class_exists( 'WC_Payments_Subscriptions' ) ) { $stripe_billing_migrator = WC_Payments_Subscriptions::get_stripe_billing_migrator(); if ( $stripe_billing_migrator ) { $is_migrating_stripe_billing = $stripe_billing_migrator->is_migrating(); $stripe_billing_subscription_count = $stripe_billing_migrator->get_stripe_billing_subscription_count(); } } } return new WP_REST_Response( [ 'enabled_payment_method_ids' => $enabled_payment_methods, 'available_payment_method_ids' => $available_upe_payment_methods, 'payment_method_statuses' => $this->wcpay_gateway->get_upe_enabled_payment_method_statuses(), 'duplicated_payment_method_ids' => $this->wcpay_gateway->find_duplicates(), 'is_wcpay_enabled' => $this->wcpay_gateway->is_enabled(), 'is_manual_capture_enabled' => 'yes' === $this->wcpay_gateway->get_option( 'manual_capture' ), 'is_test_mode_enabled' => WC_Payments::mode()->is_test(), 'is_test_mode_onboarding' => WC_Payments::mode()->is_test_mode_onboarding(), 'is_dev_mode_enabled' => WC_Payments::mode()->is_dev(), 'is_multi_currency_enabled' => WC_Payments_Features::is_customer_multi_currency_enabled(), 'is_wcpay_subscriptions_enabled' => WC_Payments_Features::is_wcpay_subscriptions_enabled(), 'is_stripe_billing_enabled' => WC_Payments_Features::is_stripe_billing_enabled(), 'is_wcpay_subscriptions_eligible' => WC_Payments_Features::is_wcpay_subscriptions_eligible(), 'is_subscriptions_plugin_active' => $this->wcpay_gateway->is_subscriptions_plugin_active(), 'is_woopay_global_theme_support_enabled' => $this->wcpay_gateway->is_woopay_global_theme_support_enabled(), 'account_country' => $this->wcpay_gateway->get_option( 'account_country' ), 'account_statement_descriptor' => $this->wcpay_gateway->get_option( 'account_statement_descriptor' ), 'account_statement_descriptor_kanji' => $this->wcpay_gateway->get_option( 'account_statement_descriptor_kanji' ), 'account_statement_descriptor_kana' => $this->wcpay_gateway->get_option( 'account_statement_descriptor_kana' ), 'account_business_name' => $this->wcpay_gateway->get_option( 'account_business_name' ), 'account_business_url' => $this->wcpay_gateway->get_option( 'account_business_url' ), 'account_business_support_address' => $this->wcpay_gateway->get_option( 'account_business_support_address' ), 'account_business_support_email' => $this->wcpay_gateway->get_option( 'account_business_support_email' ), 'account_business_support_phone' => $this->wcpay_gateway->get_option( 'account_business_support_phone' ), 'account_branding_logo' => $this->wcpay_gateway->get_option( 'account_branding_logo' ), 'account_branding_icon' => $this->wcpay_gateway->get_option( 'account_branding_icon' ), 'account_branding_primary_color' => $this->wcpay_gateway->get_option( 'account_branding_primary_color' ), 'account_branding_secondary_color' => $this->wcpay_gateway->get_option( 'account_branding_secondary_color' ), 'account_domestic_currency' => $this->wcpay_gateway->get_option( 'account_domestic_currency' ), 'account_communications_email' => $this->wcpay_gateway->get_option( 'account_communications_email' ), 'is_payment_request_enabled' => $this->wcpay_gateway->is_payment_request_enabled(), 'is_apple_google_pay_in_payment_methods_options_enabled' => 'yes' === $this->wcpay_gateway->get_option( 'apple_google_pay_in_payment_methods_options' ), 'is_debug_log_enabled' => 'yes' === $this->wcpay_gateway->get_option( 'enable_logging' ), 'payment_request_button_size' => $this->wcpay_gateway->get_option( 'payment_request_button_size' ), 'payment_request_button_type' => $this->wcpay_gateway->get_option( 'payment_request_button_type' ), 'payment_request_button_theme' => $this->wcpay_gateway->get_option( 'payment_request_button_theme' ), 'payment_request_button_border_radius' => $this->wcpay_gateway->get_option( 'payment_request_button_border_radius', WC_Payments_Express_Checkout_Button_Handler::DEFAULT_BORDER_RADIUS_IN_PX ), 'is_saved_cards_enabled' => $this->wcpay_gateway->is_saved_cards_enabled(), 'is_card_present_eligible' => $this->wcpay_gateway->is_card_present_eligible() && isset( WC()->payment_gateways()->get_available_payment_gateways()['cod'] ), 'is_woopay_enabled' => WC_Payments_Features::is_woopay_eligible() && 'yes' === $this->wcpay_gateway->get_option( 'platform_checkout' ), 'is_amazon_pay_enabled' => $this->is_amazon_pay_enabled(), 'show_woopay_incompatibility_notice' => get_option( 'woopay_invalid_extension_found', false ), 'woopay_custom_message' => $this->wcpay_gateway->get_option( 'platform_checkout_custom_message' ), 'woopay_store_logo' => $this->wcpay_gateway->get_option( 'platform_checkout_store_logo' ), 'deposit_schedule_interval' => $this->wcpay_gateway->get_option( 'deposit_schedule_interval' ), 'deposit_schedule_monthly_anchor' => $this->wcpay_gateway->get_option( 'deposit_schedule_monthly_anchor' ), 'deposit_schedule_weekly_anchor' => $this->wcpay_gateway->get_option( 'deposit_schedule_weekly_anchor' ), 'deposit_delay_days' => $this->wcpay_gateway->get_option( 'deposit_delay_days' ), 'deposit_status' => $this->wcpay_gateway->get_option( 'deposit_status' ), 'deposit_restrictions' => $this->wcpay_gateway->get_option( 'deposit_restrictions' ), 'deposit_completed_waiting_period' => $this->wcpay_gateway->get_option( 'deposit_completed_waiting_period' ), 'current_protection_level' => $this->wcpay_gateway->get_option( 'current_protection_level' ), 'advanced_fraud_protection_settings' => $this->wcpay_gateway->get_option( 'advanced_fraud_protection_settings' ), 'is_migrating_stripe_billing' => $is_migrating_stripe_billing ?? false, 'stripe_billing_subscription_count' => $stripe_billing_subscription_count ?? 0, 'stripe_billing_migrated_count' => $stripe_billing_migrated_count ?? 0, 'express_checkout_product_methods' => $this->wcpay_gateway->get_option( 'express_checkout_product_methods', [] ), 'express_checkout_cart_methods' => $this->wcpay_gateway->get_option( 'express_checkout_cart_methods', [] ), 'express_checkout_checkout_methods' => $this->wcpay_gateway->get_option( 'express_checkout_checkout_methods', [] ), ] ); } /** * Update settings. * * @param WP_REST_Request $request Full data about the request. */ public function update_settings( WP_REST_Request $request ) { $this->update_is_wcpay_enabled( $request ); $this->update_enabled_payment_methods( $request ); $this->update_is_manual_capture_enabled( $request ); $this->update_is_test_mode_enabled( $request ); $this->update_is_debug_log_enabled( $request ); $this->update_is_multi_currency_enabled( $request ); $this->update_is_wcpay_subscriptions_enabled( $request ); $this->update_is_payment_request_enabled( $request ); $this->update_is_apple_google_pay_in_payment_methods_options_enabled( $request ); $this->update_payment_request_appearance( $request ); $this->update_is_saved_cards_enabled( $request ); $this->update_is_woopay_enabled( $request ); $this->update_is_amazon_pay_enabled( $request ); $this->update_is_woopay_global_theme_support_enabled( $request ); $this->update_woopay_store_logo( $request ); $this->update_woopay_custom_message( $request ); $this->track_woopay_enabled_locations( $request ); $this->update_express_checkout_location_methods( $request ); // Note: Both "current_protection_level" and "advanced_fraud_protection_settings" // are handled in the below method. $this->update_fraud_protection_settings( $request ); $this->update_is_stripe_billing_enabled( $request ); $update_account_result = $this->update_account( $request ); if ( is_wp_error( $update_account_result ) ) { return new WP_REST_Response( [ 'server_error' => $update_account_result->get_error_message() ], 400 ); } // Sync the store setup with the Transact Platform. $this->account->store_setup_sync(); return new WP_REST_Response( $this->get_settings(), 200 ); } /** * Schedule a migration of Stripe Billing subscriptions. * * @param WP_REST_Request|null $request The request object. Optional. If passed, the function will return a REST response. * * @return WP_REST_Response|null The response object, if this is a REST request. */ public function schedule_stripe_billing_migration( ?WP_REST_Request $request = null ) { if ( class_exists( 'WC_Payments_Subscriptions' ) ) { $stripe_billing_migrator = WC_Payments_Subscriptions::get_stripe_billing_migrator(); if ( $stripe_billing_migrator && ! $stripe_billing_migrator->is_migrating() && $stripe_billing_migrator->get_stripe_billing_subscription_count() > 0 ) { $stripe_billing_migrator->schedule_migrate_wcpay_subscriptions_action(); } } // Return a response if this is a REST request. if ( $request ) { return new WP_REST_Response( [], 200 ); } } /** * Updates WooPayments enabled status. * * @param WP_REST_Request $request Request object. */ private function update_is_wcpay_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_wcpay_enabled' ) ) { return; } $is_wcpay_enabled = $request->get_param( 'is_wcpay_enabled' ); if ( $is_wcpay_enabled ) { $this->wcpay_gateway->enable(); } else { $this->wcpay_gateway->disable(); } } /** * Updates the list of enabled payment methods. * * @param WP_REST_Request $request Request object. */ private function update_enabled_payment_methods( WP_REST_Request $request ) { if ( ! $request->has_param( 'enabled_payment_method_ids' ) ) { return; } $payment_method_ids_to_enable = $request->get_param( 'enabled_payment_method_ids' ); $available_payment_methods = $this->wcpay_gateway->get_upe_available_payment_methods(); // Only 'card' and 'link' support manual capture. Leave them enabled if they're already enabled. if ( $request->has_param( 'is_manual_capture_enabled' ) && $request->get_param( 'is_manual_capture_enabled' ) ) { $payment_method_ids_to_enable = array_intersect( $payment_method_ids_to_enable, [ Payment_Method::CARD, Payment_Method::LINK ] ); } $payment_method_ids_to_enable = array_values( array_filter( $payment_method_ids_to_enable, function ( $payment_method ) use ( $available_payment_methods ) { return in_array( $payment_method, $available_payment_methods, true ); } ) ); $this->request_unrequested_payment_methods( $payment_method_ids_to_enable ); $active_payment_methods = $this->wcpay_gateway->get_upe_enabled_payment_method_ids(); $disabled_payment_methods = array_diff( $active_payment_methods, $payment_method_ids_to_enable ); $enabled_payment_methods = array_diff( $payment_method_ids_to_enable, $active_payment_methods ); // Log Tracks events for each enabled/disabled payment method. // We log these events before actually enabling/disabling the payment methods // to ensure that if enabling/disabling fails, we still have a record of the // attempt. $pm_to_capability_key_map = $this->wcpay_gateway->get_payment_method_capability_key_map(); foreach ( $disabled_payment_methods as $disabled_payment_method ) { $this->tracks_event( Track_Events::PAYMENT_METHOD_DISABLED, [ 'payment_method_id' => $disabled_payment_method, 'capability_id' => $pm_to_capability_key_map[ $disabled_payment_method ] ?? null, ] ); } foreach ( $enabled_payment_methods as $enabled_payment_method ) { $this->tracks_event( Track_Events::PAYMENT_METHOD_ENABLED, [ 'payment_method_id' => $enabled_payment_method, 'capability_id' => $pm_to_capability_key_map[ $enabled_payment_method ] ?? null, ] ); } foreach ( $enabled_payment_methods as $payment_method_id ) { $gateway = WC_Payments::get_payment_gateway_by_id( $payment_method_id ); if ( ! $gateway ) { /* translators: 1: Payment method ID, 2: Error message */ WC_Payments_Utils::log_to_wc( sprintf( 'Failed to enable payment method %1$s: %2$s', $payment_method_id, 'payment gateway instance not available' ), 'warning' ); continue; } // Try to activate any promotions for this payment method BEFORE enabling it. // This is done first because visible promotions are filtered out for already-enabled PMs. // The service method handles its own exception catching, logging, and tracking internally. $this->pm_promotions_service->maybe_activate_promotion_for_payment_method( $payment_method_id ); $gateway->enable(); } foreach ( $disabled_payment_methods as $payment_method_id ) { $gateway = WC_Payments::get_payment_gateway_by_id( $payment_method_id ); if ( ! $gateway ) { /* translators: 1: Payment method ID, 2: Error message */ WC_Payments_Utils::log_to_wc( sprintf( 'Failed to disable payment method %1$s: %2$s', $payment_method_id, 'payment gateway instance not available' ), 'warning' ); continue; } $gateway->disable(); } // Keep the enabled payment method IDs list synchronized across gateway setting objects unless we remove this list with all dependencies. foreach ( WC_Payments::get_payment_gateway_map() as $payment_gateway ) { $payment_gateway->update_option( 'upe_enabled_payment_method_ids', $payment_method_ids_to_enable ); } } /** * Requests the capabilities of unrequested payment methods * * @param array $payment_method_ids_to_enable Enabled Payment method ID's. * * @return void */ private function request_unrequested_payment_methods( $payment_method_ids_to_enable ) { $capability_key_map = $this->wcpay_gateway->get_payment_method_capability_key_map(); $payment_method_statuses = $this->wcpay_gateway->get_upe_enabled_payment_method_statuses(); $cache_needs_refresh = false; foreach ( $payment_method_ids_to_enable as $payment_method_id_to_enable ) { $stripe_key = $capability_key_map[ $payment_method_id_to_enable ] ?? null; if ( array_key_exists( $stripe_key, $payment_method_statuses ) ) { if ( 'unrequested' === $payment_method_statuses[ $stripe_key ]['status'] ) { $request_result = $this->api_client->request_capability( $stripe_key, true ); $cache_needs_refresh = $cache_needs_refresh || 'unrequested' !== $request_result['status']; } } } if ( $cache_needs_refresh ) { $this->wcpay_gateway->refresh_cached_account_data(); } } /** * Updates WooPayments manual capture. * * @param WP_REST_Request $request Request object. */ private function update_is_manual_capture_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_manual_capture_enabled' ) ) { return; } $is_manual_capture_enabled = $request->get_param( 'is_manual_capture_enabled' ); $this->wcpay_gateway->update_option( 'manual_capture', $is_manual_capture_enabled ? 'yes' : 'no' ); } /** * Updates WooPayments test mode. * * @param WP_REST_Request $request Request object. */ private function update_is_test_mode_enabled( WP_REST_Request $request ) { // Avoid updating test mode when dev mode is enabled. if ( WC_Payments::mode()->is_dev() ) { return; } if ( ! $request->has_param( 'is_test_mode_enabled' ) ) { return; } $is_test_mode_enabled = $request->get_param( 'is_test_mode_enabled' ); $this->wcpay_gateway->update_option( 'test_mode', $is_test_mode_enabled ? 'yes' : 'no' ); } /** * Updates WooPayments test mode. * * @param WP_REST_Request $request Request object. */ private function update_is_debug_log_enabled( WP_REST_Request $request ) { // Avoid updating test mode when dev mode is enabled. if ( WC_Payments::mode()->is_dev() ) { return; } if ( ! $request->has_param( 'is_debug_log_enabled' ) ) { return; } $is_debug_log_enabled = $request->get_param( 'is_debug_log_enabled' ); $this->wcpay_gateway->update_option( 'enable_logging', $is_debug_log_enabled ? 'yes' : 'no' ); } /** * Updates customer Multi-Currency feature status. * * @param WP_REST_Request $request Request object. */ private function update_is_multi_currency_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_multi_currency_enabled' ) ) { return; } $is_multi_currency_enabled = $request->get_param( 'is_multi_currency_enabled' ); update_option( '_wcpay_feature_customer_multi_currency', $is_multi_currency_enabled ? '1' : '0' ); } /** * Updates the WCPay Subscriptions feature status. * * @param WP_REST_Request $request Request object. */ private function update_is_wcpay_subscriptions_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_wcpay_subscriptions_enabled' ) ) { return; } $is_wcpay_subscriptions_enabled = $request->get_param( 'is_wcpay_subscriptions_enabled' ); // Prevent enabling bundled subscriptions - feature has been removed in 10.2.0. // Only allow disabling the feature if it was previously enabled. if ( ! $is_wcpay_subscriptions_enabled ) { update_option( WC_Payments_Features::WCPAY_SUBSCRIPTIONS_FLAG_NAME, '0' ); } } /** * Updates WooPayments account fields * * @param WP_REST_Request $request Request object. */ private function update_account( WP_REST_Request $request ) { $updated_fields_callback = function ( $value, string $key ) { return array_key_exists( $key, WC_Payment_Gateway_WCPay::ACCOUNT_SETTINGS_MAPPING ) && $this->wcpay_gateway->get_option( $key ) !== $value; }; // Filter out fields that are unchanged or not in the list of fields to update. $updated_fields = array_filter( $request->get_params(), $updated_fields_callback, ARRAY_FILTER_USE_BOTH ); // If we are updating an anchor for the deposit schedule then we must also send through the interval. if ( ! isset( $updated_fields['deposit_schedule_interval'] ) && array_intersect( array_keys( $updated_fields ), [ 'deposit_schedule_monthly_anchor', 'deposit_schedule_weekly_anchor' ] ) ) { $updated_fields['deposit_schedule_interval'] = $this->wcpay_gateway->get_option( 'deposit_schedule_interval' ); } // If we are updating any deposit schedule values, we should invalidate the next deposit notice dismissed notice option. if ( preg_grep( '/^deposit_schedule_/', array_keys( $updated_fields ) ) ) { delete_option( 'wcpay_next_deposit_notice_dismissed' ); } return $this->wcpay_gateway->update_account_settings( $updated_fields ); } /** * Updates the "payment request" enable/disable settings. * * @param WP_REST_Request $request Request object. */ private function update_is_payment_request_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_payment_request_enabled' ) ) { return; } $is_payment_request_enabled = $request->get_param( 'is_payment_request_enabled' ); // Update Google Pay and Apple Pay enabled settings to keep them in sync. $google_pay_gateway = WC_Payments::get_payment_gateway_by_id( \WCPay\PaymentMethods\Configs\Definitions\GooglePayDefinition::get_id() ); $apple_pay_gateway = WC_Payments::get_payment_gateway_by_id( \WCPay\PaymentMethods\Configs\Definitions\ApplePayDefinition::get_id() ); if ( $is_payment_request_enabled ) { if ( $google_pay_gateway ) { $google_pay_gateway->enable(); } if ( $apple_pay_gateway ) { $apple_pay_gateway->enable(); } } else { if ( $google_pay_gateway ) { $google_pay_gateway->disable(); } if ( $apple_pay_gateway ) { $apple_pay_gateway->disable(); } } } /** * Updates the "Apple Pay / Google Pay in payment methods options" enable/disable settings. * * @param WP_REST_Request $request Request object. */ private function update_is_apple_google_pay_in_payment_methods_options_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_apple_google_pay_in_payment_methods_options_enabled' ) ) { return; } $is_apple_google_pay_in_payment_methods_options_enabled = $request->get_param( 'is_apple_google_pay_in_payment_methods_options_enabled' ); $this->wcpay_gateway->update_option( 'apple_google_pay_in_payment_methods_options', $is_apple_google_pay_in_payment_methods_options_enabled ? 'yes' : 'no' ); } /** * Updates appearance attributes of the payment request button. * * @param WP_REST_Request $request Request object. */ private function update_payment_request_appearance( WP_REST_Request $request ) { $attributes = [ 'payment_request_button_type' => 'payment_request_button_type', 'payment_request_button_size' => 'payment_request_button_size', 'payment_request_button_theme' => 'payment_request_button_theme', 'payment_request_button_border_radius' => 'payment_request_button_border_radius', ]; foreach ( $attributes as $request_key => $attribute ) { if ( ! $request->has_param( $request_key ) ) { continue; } $value = $request->get_param( $request_key ); $this->wcpay_gateway->update_option( $attribute, $value ); } } /** * Updates WooPayments "saved cards" feature. * * @param WP_REST_Request $request Request object. */ private function update_is_saved_cards_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_saved_cards_enabled' ) ) { return; } $is_saved_cards_enabled = $request->get_param( 'is_saved_cards_enabled' ); $this->wcpay_gateway->update_option( 'saved_cards', $is_saved_cards_enabled ? 'yes' : 'no' ); } /** * Updates the "woopay" enable/disable settings. * * @param WP_REST_Request $request Request object. */ private function update_is_woopay_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_woopay_enabled' ) ) { return; } $is_woopay_enabled = $request->get_param( 'is_woopay_enabled' ); $this->wcpay_gateway->update_is_woopay_enabled( $is_woopay_enabled ); } /** * Updates the "Amazon Pay" enable/disable settings. * * @param WP_REST_Request $request Request object. */ private function update_is_amazon_pay_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_amazon_pay_enabled' ) ) { return; } $amazon_pay_gateway = WC_Payments::get_payment_gateway_by_id( \WCPay\PaymentMethods\Configs\Definitions\AmazonPayDefinition::get_id() ); if ( ! $amazon_pay_gateway ) { return; } $is_amazon_pay_enabled = $request->get_param( 'is_amazon_pay_enabled' ); if ( $is_amazon_pay_enabled ) { $amazon_pay_gateway->enable(); $this->request_unrequested_payment_methods( [ 'amazon_pay' ] ); } else { $amazon_pay_gateway->disable(); } } /** * Checks if Amazon Pay is enabled. * * @return bool */ private function is_amazon_pay_enabled(): bool { $amazon_pay_gateway = WC_Payments::get_payment_gateway_by_id( \WCPay\PaymentMethods\Configs\Definitions\AmazonPayDefinition::get_id() ); return $amazon_pay_gateway && $amazon_pay_gateway->is_enabled(); } /** * Updates the WooPay Global Theme Support enable/disable settings. * * @param WP_REST_Request $request Request object. */ private function update_is_woopay_global_theme_support_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_woopay_global_theme_support_enabled' ) ) { return; } $value = $request->get_param( 'is_woopay_global_theme_support_enabled' ); $this->wcpay_gateway->update_is_woopay_global_theme_support_enabled( $value ); } /** * Updates the custom message that will appear for woopay customers. * * @param WP_REST_Request $request Request object. */ private function update_woopay_custom_message( WP_REST_Request $request ) { if ( ! $request->has_param( 'woopay_custom_message' ) ) { return; } $woopay_custom_message = $request->get_param( 'woopay_custom_message' ); $woopay_custom_message = str_replace( '[terms_of_service_link]', '[terms]', $woopay_custom_message ); $woopay_custom_message = str_replace( '[privacy_policy_link]', '[privacy_policy]', $woopay_custom_message ); $this->wcpay_gateway->update_option( 'platform_checkout_custom_message', $woopay_custom_message ); } /** * Updates the store logo that will appear for woopay customers. * * @param WP_REST_Request $request Request object. */ private function update_woopay_store_logo( WP_REST_Request $request ) { if ( ! $request->has_param( 'woopay_store_logo' ) ) { return; } $woopay_store_logo = $request->get_param( 'woopay_store_logo' ); $this->wcpay_gateway->update_option( 'platform_checkout_store_logo', $woopay_store_logo ); } /** * Tracks changes to WooPay enabled locations for analytics. * * Derives the WooPay locations from the new location-centric express checkout settings. * * @param WP_REST_Request $request Request object. */ private function track_woopay_enabled_locations( WP_REST_Request $request ) { $is_woopay_enabled = WC_Payments_Features::is_woopay_enabled(); if ( ! $is_woopay_enabled ) { return; } $locations = [ 'product', 'cart', 'checkout' ]; $has_location_param = false; foreach ( $locations as $location ) { if ( $request->has_param( "express_checkout_{$location}_methods" ) ) { $has_location_param = true; break; } } if ( ! $has_location_param ) { return; } // Build the list of locations where WooPay is enabled. $woopay_enabled_locations = []; foreach ( $locations as $location ) { $param_name = "express_checkout_{$location}_methods"; if ( $request->has_param( $param_name ) ) { $methods = $request->get_param( $param_name ); if ( is_array( $methods ) && in_array( 'woopay', $methods, true ) ) { $woopay_enabled_locations[] = $location; } } else { // Fall back to current saved setting if not in request. $methods = $this->wcpay_gateway->get_option( $param_name, [] ); if ( is_array( $methods ) && in_array( 'woopay', $methods, true ) ) { $woopay_enabled_locations[] = $location; } } } // All possible locations for tracking comparison. $all_locations = array_combine( $locations, $locations ); WC_Payments::woopay_tracker()->woopay_locations_updated( $all_locations, $woopay_enabled_locations ); } /** * Updates the express checkout methods enabled at each location. * * @param WP_REST_Request $request Request object. */ private function update_express_checkout_location_methods( WP_REST_Request $request ) { $locations = [ 'product', 'cart', 'checkout' ]; foreach ( $locations as $location ) { $param_name = "express_checkout_{$location}_methods"; if ( ! $request->has_param( $param_name ) ) { continue; } $methods = $request->get_param( $param_name ); $this->wcpay_gateway->update_option( $param_name, $methods ); } } /** * Updates the settings of fraud protection rules (both settings and level in one function, because they are connected). * * @param WP_REST_Request $request Request object. * @throws InvalidArgumentException If the ruleset configuration is invalid. */ private function update_fraud_protection_settings( WP_REST_Request $request ) { if ( ! $request->has_param( 'current_protection_level' ) || ! $request->has_param( 'advanced_fraud_protection_settings' ) ) { return; } $protection_level = $request->get_param( 'current_protection_level' ); // Check validity of the protection level. if ( ! in_array( $protection_level, [ 'basic', 'standard', 'high', 'advanced' ], true ) ) { return; } // Get rulesets per protection level. switch ( $protection_level ) { case 'basic': $ruleset_config = Fraud_Risk_Tools::get_basic_protection_settings(); break; case 'standard': $ruleset_config = Fraud_Risk_Tools::get_standard_protection_settings(); break; case 'high': $ruleset_config = Fraud_Risk_Tools::get_high_protection_settings(); break; case 'advanced': $received_ruleset = $request->get_param( 'advanced_fraud_protection_settings' ); if ( ! is_array( $received_ruleset ) ) { throw new InvalidArgumentException( 'Invalid ruleset configuration' ); } foreach ( $received_ruleset as $rule ) { if ( ! Rule::validate_array( $rule ) ) { throw new InvalidArgumentException( 'Invalid ruleset configuration' ); } } $ruleset_config = $received_ruleset; break; } // Save ruleset to the server. $this->api_client->save_fraud_ruleset( $ruleset_config ); // Update local cache. $this->wcpay_gateway->update_cached_account_data( 'fraud_mitigation_settings', [ 'avs_check_enabled' => $this->get_avs_check_enabled( $ruleset_config ) ] ); delete_transient( 'wcpay_fraud_protection_settings' ); set_transient( 'wcpay_fraud_protection_settings', $ruleset_config, 1 * DAY_IN_SECONDS ); // Update the option only when server update succeeds. update_option( 'current_protection_level', $protection_level ); } /** * Updates the Stripe Billing Subscriptions feature status. * * @param WP_REST_Request $request Request object. */ private function update_is_stripe_billing_enabled( WP_REST_Request $request ) { if ( ! $request->has_param( 'is_stripe_billing_enabled' ) ) { return; } $is_stripe_billing_enabled = $request->get_param( 'is_stripe_billing_enabled' ); update_option( WC_Payments_Features::STRIPE_BILLING_FLAG_NAME, $is_stripe_billing_enabled ? '1' : '0' ); // Schedule a migration if Stripe Billing was disabled and there are subscriptions to migrate. if ( ! $is_stripe_billing_enabled ) { $this->schedule_stripe_billing_migration(); } } /** * Get the AVS check enabled status from the ruleset config. * * @param array $ruleset_config The ruleset config. * * @return bool */ private function get_avs_check_enabled( array $ruleset_config ) { $avs_check_enabled = false; foreach ( $ruleset_config as $rule_definition ) { if ( 'avs_verification' === $rule_definition['key'] ) { $avs_check_enabled = true; break; } } return $avs_check_enabled; } /** * Send a Tracks event. * * By default Woo adds `url`, `blog_lang`, `blog_id`, `store_id`, `products_count`, and `wc_version` * properties to every event. * * @todo This is a duplicate of the one in the WC_Payments_Account and WC_Payments_Onboarding_Service classes. * * @param string $name The event name. * @param array $properties Optional. The event custom properties. * * @return void */ private function tracks_event( string $name, array $properties = [] ) { if ( ! function_exists( 'wc_admin_record_tracks_event' ) ) { return; } // Add default properties to every event. $account_service = WC_Payments::get_account_service(); $tracking_info = $account_service ? $account_service->get_tracking_info() : []; $properties = array_merge( $properties, [ 'is_test_mode' => WC_Payments::mode()->is_test(), 'jetpack_connected' => $this->api_client->is_server_connected(), 'wcpay_version' => WCPAY_VERSION_NUMBER, 'woo_country_code' => WC()->countries->get_base_country(), ], $tracking_info ?? [] ); wc_admin_record_tracks_event( $name, $properties ); Logger::info( 'Tracks event: ' . $name . ' with data: ' . wp_json_encode( WC_Payments_Utils::redact_array( $properties, [ 'woo_country_code' ] ) ) ); } } includes/class-wc-payment-gateway-wcpay.php 0000777 00000546565 15251156605 0015074 0 ustar 00 <?php /** * Class WC_Payment_Gateway_WCPay * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use Automattic\WooCommerce\StoreApi\Payments\PaymentContext; use Automattic\WooCommerce\StoreApi\Payments\PaymentResult; use WCPay\Constants\Country_Code; use WCPay\Constants\Fraud_Meta_Box_Type; use WCPay\Constants\Order_Mode; use WCPay\Constants\Order_Status; use WCPay\Constants\Payment_Capture_Type; use WCPay\Constants\Payment_Initiated_By; use WCPay\Constants\Intent_Status; use WCPay\Constants\Payment_Type; use WCPay\Constants\Payment_Method; use WCPay\Constants\Refund_Status; use WCPay\Exceptions\{Add_Payment_Method_Exception, Amount_Too_Small_Exception, API_Merchant_Exception, Process_Payment_Exception, Intent_Authentication_Exception, API_Exception, Invalid_Address_Exception, Fraud_Prevention_Enabled_Exception, Invalid_Phone_Number_Exception, Rate_Limiter_Enabled_Exception, Order_ID_Mismatch_Exception, Order_Not_Found_Exception}; use WCPay\Core\Server\Request\Cancel_Intention; use WCPay\Core\Server\Request\Capture_Intention; use WCPay\Core\Server\Request\Create_And_Confirm_Intention; use WCPay\Core\Server\Request\Create_And_Confirm_Setup_Intention; use WCPay\Core\Server\Request\Get_Charge; use WCPay\Core\Server\Request\Get_Intention; use WCPay\Core\Server\Request\Get_Setup_Intention; use WCPay\Core\Server\Request\List_Charge_Refunds; use WCPay\Core\Server\Request\Refund_Charge; use WCPay\Duplicate_Payment_Prevention_Service; use WCPay\Duplicates_Detection_Service; use WCPay\Fraud_Prevention\Fraud_Prevention_Service; use WCPay\Fraud_Prevention\Fraud_Risk_Tools; use WCPay\Logger; use WCPay\Payment_Information; use WCPay\WooPay\WooPay_Order_Status_Sync; use WCPay\WooPay\WooPay_Utilities; use WCPay\Session_Rate_Limiter; use WCPay\Tracker; use WCPay\Internal\Service\Level3Service; use WCPay\Internal\Service\OrderService; use WCPay\Payment_Methods\CC_Payment_Method; use WCPay\Payment_Methods\UPE_Payment_Method; use WCPay\PaymentMethods\Configs\Definitions\LinkDefinition; use WCPay\PaymentMethods\Configs\Registry\PaymentMethodDefinitionRegistry; /** * Gateway class for WooPayments */ class WC_Payment_Gateway_WCPay extends WC_Payment_Gateway_CC { use WC_Payment_Gateway_WCPay_Subscriptions_Trait; /** * Internal ID of the payment gateway. * * @type string */ const GATEWAY_ID = 'woocommerce_payments'; const METHOD_ENABLED_KEY = 'enabled'; /** * Mapping between the client and server accepted params: * - Keys are WCPay client accepted params (in WC_REST_Payments_Settings_Controller). * - Values are WCPay Server accepted params. * * @type array */ const ACCOUNT_SETTINGS_MAPPING = [ 'account_statement_descriptor' => 'statement_descriptor', 'account_statement_descriptor_kanji' => 'statement_descriptor_kanji', 'account_statement_descriptor_kana' => 'statement_descriptor_kana', 'account_business_name' => 'business_name', 'account_business_url' => 'business_url', 'account_business_support_address' => 'business_support_address', 'account_business_support_email' => 'business_support_email', 'account_business_support_phone' => 'business_support_phone', 'account_branding_logo' => 'branding_logo', 'account_branding_icon' => 'branding_icon', 'account_branding_primary_color' => 'branding_primary_color', 'account_branding_secondary_color' => 'branding_secondary_color', 'account_communications_email' => 'communications_email', 'deposit_schedule_interval' => 'deposit_schedule_interval', 'deposit_schedule_weekly_anchor' => 'deposit_schedule_weekly_anchor', 'deposit_schedule_monthly_anchor' => 'deposit_schedule_monthly_anchor', ]; const UPDATE_SAVED_PAYMENT_METHOD = 'wcpay_update_saved_payment_method'; /** * Set a large limit argument for retrieving user tokens. * * @type int */ const USER_FORMATTED_TOKENS_LIMIT = 100; const PROCESS_REDIRECT_ORDER_MISMATCH_ERROR_CODE = 'upe_process_redirect_order_id_mismatched'; const UPE_APPEARANCE_TRANSIENT = 'wcpay_upe_appearance'; const UPE_ADD_PAYMENT_METHOD_APPEARANCE_TRANSIENT = 'wcpay_upe_add_payment_method_appearance'; const WC_BLOCKS_UPE_APPEARANCE_TRANSIENT = 'wcpay_wc_blocks_upe_appearance'; const UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT = 'wcpay_upe_bnpl_product_page_appearance'; const UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT = 'wcpay_upe_bnpl_classic_cart_appearance'; const UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT = 'wcpay_upe_bnpl_cart_block_appearance'; const UPE_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_appearance_theme'; const UPE_ADD_PAYMENT_METHOD_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_add_payment_method_appearance_theme'; const WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT = 'wcpay_wc_blocks_upe_appearance_theme'; const UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_bnpl_product_page_appearance_theme'; const UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_bnpl_classic_cart_appearance_theme'; const UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT = 'wcpay_upe_bnpl_cart_block_appearance_theme'; /** * The locations of appearance transients. */ const APPEARANCE_THEME_TRANSIENTS = [ 'checkout' => [ 'blocks' => self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT, 'classic' => self::UPE_APPEARANCE_THEME_TRANSIENT, ], 'product_page' => [ 'blocks' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT, 'classic' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT, ], 'cart' => [ 'blocks' => self::UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT, 'classic' => self::UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT, ], ]; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ protected $payments_api_client; /** * WC_Payments_Account instance to get information about the account * * @var WC_Payments_Account */ protected $account; /** * WC_Payments_Customer instance for working with customer information * * @var WC_Payments_Customer_Service */ protected $customer_service; /** * WC_Payments_Token instance for working with customer tokens * * @var WC_Payments_Token_Service */ protected $token_service; /** * WC_Payments_Order_Service instance * * @var WC_Payments_Order_Service */ protected $order_service; /** * WC_Payments_Action_Scheduler_Service instance for scheduling ActionScheduler jobs. * * @var WC_Payments_Action_Scheduler_Service */ private $action_scheduler_service; /** * Session_Rate_Limiter instance for limiting failed transactions. * * @var Session_Rate_Limiter */ protected $failed_transaction_rate_limiter; /** * Mapping between capability keys and payment type keys * * @var array */ protected $payment_method_capability_key_map; /** * WooPay utilities. * * @var WooPay_Utilities */ protected $woopay_util; /** * Duplicate payment prevention service. * * @var Duplicate_Payment_Prevention_Service */ protected $duplicate_payment_prevention_service; /** * Duplicate payment methods detection service * * @var Duplicates_Detection_Service */ protected $duplicate_payment_methods_detection_service; /** * WC_Payments_Localization_Service instance. * * @var WC_Payments_Localization_Service */ protected $localization_service; /** * WC_Payments_Fraud_Service instance to get information about fraud services. * * @var WC_Payments_Fraud_Service */ protected $fraud_service; /** * UPE Payment Method for gateway. * * @var UPE_Payment_Method */ protected $payment_method; /** * Array mapping payment method string IDs to classes * * @var UPE_Payment_Method[] */ protected $payment_methods = []; /** * Stripe payment method type ID. * * @var string */ protected $stripe_id; /** * WC_Payment_Gateway_WCPay constructor. * * @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client. * @param WC_Payments_Account $account - Account class instance. * @param WC_Payments_Customer_Service $customer_service - Customer class instance. * @param WC_Payments_Token_Service $token_service - Token class instance. * @param WC_Payments_Action_Scheduler_Service $action_scheduler_service - Action Scheduler service instance. * @param UPE_Payment_Method $payment_method - Specific UPE_Payment_Method instance for gateway. * @param array $payment_methods - Array of UPE payment methods. * @param WC_Payments_Order_Service $order_service - Order class instance. * @param Duplicate_Payment_Prevention_Service $duplicate_payment_prevention_service - Service for preventing duplicate payments. * @param WC_Payments_Localization_Service $localization_service - Localization service instance. * @param WC_Payments_Fraud_Service $fraud_service - Fraud service instance. * @param Duplicates_Detection_Service $duplicate_payment_methods_detection_service - Service for finding duplicate enabled payment methods. * @param Session_Rate_Limiter|null $failed_transaction_rate_limiter - Rate Limiter for failed transactions. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account, WC_Payments_Customer_Service $customer_service, WC_Payments_Token_Service $token_service, WC_Payments_Action_Scheduler_Service $action_scheduler_service, UPE_Payment_Method $payment_method, array $payment_methods, WC_Payments_Order_Service $order_service, Duplicate_Payment_Prevention_Service $duplicate_payment_prevention_service, WC_Payments_Localization_Service $localization_service, WC_Payments_Fraud_Service $fraud_service, Duplicates_Detection_Service $duplicate_payment_methods_detection_service, ?Session_Rate_Limiter $failed_transaction_rate_limiter = null ) { $this->payment_methods = $payment_methods; $this->payment_method = $payment_method; $this->stripe_id = $payment_method->get_id(); $this->payments_api_client = $payments_api_client; $this->account = $account; $this->customer_service = $customer_service; $this->token_service = $token_service; $this->action_scheduler_service = $action_scheduler_service; $this->failed_transaction_rate_limiter = $failed_transaction_rate_limiter; $this->order_service = $order_service; $this->duplicate_payment_prevention_service = $duplicate_payment_prevention_service; $this->localization_service = $localization_service; $this->fraud_service = $fraud_service; $this->duplicate_payment_methods_detection_service = $duplicate_payment_methods_detection_service; $this->id = static::GATEWAY_ID; $this->icon = $this->get_theme_icon(); $this->has_fields = true; $this->method_title = 'WooPayments'; $this->description = ''; $this->supports = [ 'products', 'refunds', ]; if ( 'card' !== $this->stripe_id ) { $this->id = self::GATEWAY_ID . '_' . $this->stripe_id; } /** * FLAG: PAYMENT_METHODS_LIST * Once all payment methods are converted to use definitions, they will all * have a get_stripe_id() method that can be used instead of this map. */ // Capabilities have different keys than the payment method ID's, // so instead of appending '_payments' to the end of the ID, it'll be better // to have a map for it instead, just in case the pattern changes. $this->payment_method_capability_key_map = [ 'alipay' => 'alipay_payments', 'amazon_pay' => 'amazon_pay_payments', 'apple_pay' => 'card_payments', 'sofort' => 'sofort_payments', 'giropay' => 'giropay_payments', 'google_pay' => 'card_payments', 'bancontact' => 'bancontact_payments', 'eps' => 'eps_payments', 'ideal' => 'ideal_payments', 'p24' => 'p24_payments', 'card' => 'card_payments', 'sepa_debit' => 'sepa_debit_payments', 'au_becs_debit' => 'au_becs_debit_payments', 'link' => 'link_payments', 'affirm' => 'affirm_payments', 'afterpay_clearpay' => 'afterpay_clearpay_payments', 'klarna' => 'klarna_payments', 'grabpay' => 'grabpay_payments', 'jcb' => 'jcb_payments', 'multibanco' => 'multibanco_payments', 'wechat_pay' => 'wechat_pay_payments', ]; // WooPay utilities. $this->woopay_util = new WooPay_Utilities(); // Load the settings. $this->init_settings(); // Check if subscriptions are enabled and add support for them. $this->maybe_init_subscriptions(); // If the setting to enable saved cards is enabled, then we should support tokenization and adding payment methods. if ( $this->is_saved_cards_enabled() ) { array_push( $this->supports, 'tokenization', 'add_payment_method' ); } } /** * Return the gateway's title. * * @return string */ public function get_title() { if ( ! $this->title ) { $this->title = $this->payment_method->get_title( $this->get_account_country() ); $this->method_title = "WooPayments ($this->title)"; } return parent::get_title(); } /** * Return the gateway's method title. * Constructs the title in the format "WooPayments (Payment Method Name)". * * @return string */ public function get_method_title() { $payment_method_title = $this->payment_method->get_title( $this->get_account_country() ); return "WooPayments ($payment_method_title)"; } /** * Get the form fields after they are initialized. * * @return array of options */ public function get_form_fields() { $this->form_fields = [ 'enabled' => [ 'type' => 'checkbox', 'default' => 'no', ], ]; if ( self::GATEWAY_ID === $this->id ) { $main_gateway_only_fields = [ 'account_statement_descriptor' => [ 'type' => 'account_statement_descriptor', 'title' => __( 'Customer bank statement', 'woocommerce-payments' ), 'description' => WC_Payments_Utils::esc_interpolated_html( __( 'Edit the way your store name appears on your customers’ bank statements (read more about requirements <a>here</a>).', 'woocommerce-payments' ), [ 'a' => '<a href="https://woocommerce.com/document/woopayments/customization-and-translation/bank-statement-descriptor/" target="_blank" rel="noopener noreferrer">' ] ), ], 'manual_capture' => [ 'title' => __( 'Manual capture', 'woocommerce-payments' ), 'label' => __( 'Issue an authorization on checkout, and capture later.', 'woocommerce-payments' ), 'type' => 'checkbox', 'description' => __( 'Charge must be captured within 7 days of authorization, otherwise the authorization and order will be canceled.', 'woocommerce-payments' ), 'default' => 'no', ], 'saved_cards' => [ 'title' => __( 'Saved cards', 'woocommerce-payments' ), 'label' => __( 'Enable payment via saved cards', 'woocommerce-payments' ), 'type' => 'checkbox', 'description' => __( 'If enabled, users will be able to pay with a saved card during checkout. Card details are saved on our platform, not on your store.', 'woocommerce-payments' ), 'default' => 'yes', 'desc_tip' => true, ], 'test_mode' => [ 'title' => __( 'Test mode', 'woocommerce-payments' ), 'label' => __( 'Enable test mode', 'woocommerce-payments' ), 'type' => 'checkbox', 'description' => __( 'Simulate transactions using test card numbers.', 'woocommerce-payments' ), 'default' => 'no', 'desc_tip' => true, ], 'enable_logging' => [ 'title' => __( 'Debug log', 'woocommerce-payments' ), 'label' => __( 'When enabled debug notes will be added to the log.', 'woocommerce-payments' ), 'type' => 'checkbox', 'description' => '', 'default' => 'no', ], 'payment_request_details' => [ 'title' => __( 'Payment request buttons', 'woocommerce-payments' ), 'type' => 'title', 'description' => '', ], 'payment_request_button_type' => [ 'title' => __( 'Button type', 'woocommerce-payments' ), 'type' => 'select', 'description' => __( 'Select the button type you would like to show.', 'woocommerce-payments' ), 'default' => 'default', 'desc_tip' => true, 'options' => [ 'default' => __( 'Only icon', 'woocommerce-payments' ), 'buy' => __( 'Buy', 'woocommerce-payments' ), 'donate' => __( 'Donate', 'woocommerce-payments' ), 'book' => __( 'Book', 'woocommerce-payments' ), ], ], 'payment_request_button_theme' => [ 'title' => __( 'Button theme', 'woocommerce-payments' ), 'type' => 'select', 'description' => __( 'Select the button theme you would like to show.', 'woocommerce-payments' ), 'default' => 'dark', 'desc_tip' => true, 'options' => [ 'dark' => __( 'Dark', 'woocommerce-payments' ), 'light' => __( 'Light', 'woocommerce-payments' ), 'light-outline' => __( 'Light-Outline', 'woocommerce-payments' ), ], ], 'payment_request_button_height' => [ 'title' => __( 'Button height', 'woocommerce-payments' ), 'type' => 'text', 'description' => __( 'Enter the height you would like the button to be in pixels. Width will always be 100%.', 'woocommerce-payments' ), 'default' => '44', 'desc_tip' => true, ], 'payment_request_button_label' => [ 'title' => __( 'Custom button label', 'woocommerce-payments' ), 'type' => 'text', 'description' => __( 'Enter the custom text you would like the button to have.', 'woocommerce-payments' ), 'default' => __( 'Buy now', 'woocommerce-payments' ), 'desc_tip' => true, ], 'payment_request_button_locations' => [ 'title' => __( 'Button locations', 'woocommerce-payments' ), 'type' => 'multiselect', 'description' => __( 'Select where you would like to display the button.', 'woocommerce-payments' ), 'default' => [ 'product', 'cart', 'checkout', ], 'class' => 'wc-enhanced-select', 'desc_tip' => true, 'options' => [ 'product' => __( 'Product', 'woocommerce-payments' ), 'cart' => __( 'Cart', 'woocommerce-payments' ), 'checkout' => __( 'Checkout', 'woocommerce-payments' ), ], 'custom_attributes' => [ 'data-placeholder' => __( 'Select pages', 'woocommerce-payments' ), ], ], 'upe_enabled_payment_method_ids' => [ 'title' => __( 'Payments accepted on checkout', 'woocommerce-payments' ), 'type' => 'multiselect', 'default' => [ 'card' ], 'options' => [], ], 'payment_request_button_size' => [ 'title' => __( 'Size of the button displayed for Express Checkouts', 'woocommerce-payments' ), 'type' => 'select', 'description' => __( 'Select the size of the button.', 'woocommerce-payments' ), 'default' => 'medium', 'desc_tip' => true, 'options' => [ 'small' => __( 'Small', 'woocommerce-payments' ), 'medium' => __( 'Medium', 'woocommerce-payments' ), 'large' => __( 'Large', 'woocommerce-payments' ), ], ], 'platform_checkout_custom_message' => [ 'default' => __( 'By placing this order, you agree to our [terms] and understand our [privacy_policy].', 'woocommerce-payments' ) ], 'express_checkout_product_methods' => [ 'title' => __( 'Express checkout methods on product page', 'woocommerce-payments' ), 'type' => 'multiselect', 'default' => [ 'payment_request', 'woopay', 'amazon_pay' ], 'options' => [ 'payment_request' => __( 'Apple Pay / Google Pay', 'woocommerce-payments' ), 'woopay' => __( 'WooPay', 'woocommerce-payments' ), 'amazon_pay' => __( 'Amazon Pay', 'woocommerce-payments' ), 'link' => __( 'Link', 'woocommerce-payments' ), ], ], 'express_checkout_cart_methods' => [ 'title' => __( 'Express checkout methods on cart page', 'woocommerce-payments' ), 'type' => 'multiselect', 'default' => [ 'payment_request', 'woopay', 'amazon_pay' ], 'options' => [ 'payment_request' => __( 'Apple Pay / Google Pay', 'woocommerce-payments' ), 'woopay' => __( 'WooPay', 'woocommerce-payments' ), 'amazon_pay' => __( 'Amazon Pay', 'woocommerce-payments' ), 'link' => __( 'Link', 'woocommerce-payments' ), ], ], 'express_checkout_checkout_methods' => [ 'title' => __( 'Express checkout methods on checkout page', 'woocommerce-payments' ), 'type' => 'multiselect', 'default' => [ 'payment_request', 'woopay', 'amazon_pay' ], 'options' => [ 'payment_request' => __( 'Apple Pay / Google Pay', 'woocommerce-payments' ), 'woopay' => __( 'WooPay', 'woocommerce-payments' ), 'amazon_pay' => __( 'Amazon Pay', 'woocommerce-payments' ), 'link' => __( 'Link', 'woocommerce-payments' ), ], ], ]; $this->form_fields = array_merge( $this->form_fields, $main_gateway_only_fields ); } return parent::get_form_fields(); } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_action( 'init', [ $this, 'maybe_update_properties_with_country' ] ); // Only add certain actions/filter if this is the main gateway (i.e. not split UPE). if ( self::GATEWAY_ID === $this->id ) { add_action( 'woocommerce_order_actions', [ $this, 'add_order_actions' ] ); add_action( 'woocommerce_order_action_capture_charge', [ $this, 'capture_charge' ] ); add_action( 'woocommerce_order_action_cancel_authorization', [ $this, 'cancel_authorization' ] ); add_action( 'woocommerce_order_status_cancelled', [ $this->order_service, 'cancel_authorizations_on_order_status_change' ] ); add_action( 'woocommerce_order_status_completed', [ $this->order_service, 'capture_authorization_on_order_status_change' ], 10, 3 ); add_action( 'wp_ajax_update_order_status', [ $this, 'update_order_status' ] ); add_action( 'wp_ajax_nopriv_update_order_status', [ $this, 'update_order_status' ] ); add_action( 'wp_ajax_create_setup_intent', [ $this, 'create_setup_intent_ajax' ] ); // Update the current request logged_in cookie after a guest user is created to avoid nonce inconsistencies. add_action( 'set_logged_in_cookie', [ $this, 'set_cookie_on_current_request' ] ); add_action( self::UPDATE_SAVED_PAYMENT_METHOD, [ $this, 'update_saved_payment_method' ], 10, 3 ); add_action( 'woocommerce_update_order', [ $this, 'schedule_order_tracking' ], 10, 2 ); add_action( 'woocommerce_rest_checkout_process_payment_with_context', [ $this, 'setup_payment_error_handler' ], 10, 2 ); add_filter( 'rest_request_before_callbacks', [ $this, 'remove_all_actions_on_preflight_check' ], 10, 3 ); add_action( 'woocommerce_settings_save_general', [ $this, 'update_fraud_rules_based_on_general_options' ], 20 ); } $this->maybe_init_subscriptions_hooks(); } /** * Updates icon and title using the account country. * This method runs on init is not in the controller because get_account_country might * make a request to the API if the account data is not cached. * Needed for classic checkout. * * @return void */ public function maybe_update_properties_with_country(): void { if ( \WCPay\PaymentMethods\Configs\Definitions\AfterpayDefinition::get_id() !== $this->stripe_id ) { return; } $account_country = $this->get_account_country(); $this->icon = $this->payment_method->get_icon( $account_country ); $this->title = $this->payment_method->get_title( $account_country ); } /** * Displays HTML tags for WC payment gateway radio button content. */ public function display_gateway_html() { ?> <div class="wcpay-upe-element" data-payment-method-type="<?php echo esc_attr( $this->stripe_id ); ?>"></div> <?php } /** * Renders the credit card input fields needed to get the user's payment information on the checkout page. * * We also add the JavaScript which drives the UI. */ public function payment_fields() { do_action( 'wc_payments_set_gateway', $this->get_selected_stripe_payment_type_id() ); do_action( 'wc_payments_add_upe_payment_fields' ); } /** * Adds a token to current user from a setup intent id. * * @param string $setup_intent_id ID of the setup intent. * @param WP_User $user User to add token to. * * @return WC_Payment_Token_CC|WC_Payment_Token_WCPay_SEPA|null The added token. */ public function create_token_from_setup_intent( $setup_intent_id, $user ) { try { $setup_intent_request = Get_Setup_Intention::create( $setup_intent_id ); /** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort $setup_intent = $setup_intent_request->send(); $payment_method_id = $setup_intent->get_payment_method_id(); // TODO: When adding SEPA and Sofort, we will need a new API call to get the payment method and from there get the type. // Leaving 'card' as a hardcoded value for now to avoid the extra API call. // $payment_method = $this->payment_methods['card'];// Maybe this should be enforced. $payment_method = $this->payment_method; return $payment_method->get_payment_token_for_user( $user, $payment_method_id ); } catch ( Exception $e ) { wc_add_notice( WC_Payments_Utils::get_filtered_error_message( $e ), 'error', [ 'icon' => 'error' ] ); Logger::log( 'Error when adding payment method: ' . $e->getMessage() ); } } /** * Validate order_id received from the request vs value saved in the intent metadata. * Throw an exception if they're not matched. * * @param WC_Order $order The received order to process. * @param array $intent_metadata The metadata of attached intent to the order. * * @return void * @throws Process_Payment_Exception */ private function validate_order_id_received_vs_intent_meta_order_id( WC_Order $order, array $intent_metadata ): void { $intent_meta_order_id_raw = $intent_metadata['order_id'] ?? ''; $intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0; if ( $order->get_id() !== $intent_meta_order_id ) { Logger::error( sprintf( 'UPE Process Redirect Payment - Order ID mismatched. Received: %1$d. Intent Metadata Value: %2$d', $order->get_id(), $intent_meta_order_id ) ); throw new Process_Payment_Exception( __( "We're not able to process this payment due to the order ID mismatch. Please try again later.", 'woocommerce-payments' ), self::PROCESS_REDIRECT_ORDER_MISMATCH_ERROR_CODE ); } } /** * If we're in a WooPay preflight check, remove all the checkout order processed * actions to prevent a quantity reduction of the available resources. * * @param mixed $response The response object. * @param mixed $handler The handler used for the response. * @param WP_REST_Request $request The request used to generate the response. * * @return mixed */ public function remove_all_actions_on_preflight_check( $response, $handler, $request ) { $payment_data = $this->get_request_payment_data( $request ); if ( ! empty( $payment_data['is-woopay-preflight-check'] ) ) { remove_all_actions( 'woocommerce_store_api_checkout_update_order_meta' ); remove_all_actions( 'woocommerce_store_api_checkout_order_processed' ); // Avoid increasing coupon usage count during preflight check. remove_all_actions( 'woocommerce_order_status_pending' ); // Avoid creating new accounts during preflight check. remove_all_filters( 'woocommerce_checkout_registration_required' ); add_filter( 'woocommerce_coupon_get_usage_limit', '__return_null' ); add_filter( 'woocommerce_coupon_get_usage_limit_per_user', '__return_zero' ); } return $response; } /** * Checks if the request is to the Store API checkout. * * @return bool True if the request is to the Store API checkout, false otherwise. */ protected function is_request_to_store_api_checkout() { if ( ! \WC_Payments_Utils::is_store_api_request() ) { return false; } return false !== strpos( $GLOBALS['wp']->query_vars['rest_route'], '/checkout' ); } /** * Gets and formats payment request data. * * @param \WP_REST_Request $request Request object. * @return array */ private function get_request_payment_data( \WP_REST_Request $request ) { static $payment_data = []; if ( ! empty( $payment_data ) ) { return $payment_data; } if ( ! empty( $request['payment_data'] ) ) { foreach ( $request['payment_data'] as $data ) { $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] ); } } return $payment_data; } /** * Proceed with current request using new login session (to ensure consistent nonce). * Only apply during the checkout process with the account creation. * * @param string $cookie New cookie value. */ public function set_cookie_on_current_request( $cookie ) { if ( defined( 'WOOCOMMERCE_CHECKOUT' ) && WOOCOMMERCE_CHECKOUT && did_action( 'woocommerce_created_customer' ) > 0 ) { $_COOKIE[ LOGGED_IN_COOKIE ] = $cookie; } } /** * Check if the payment gateway is connected. This method is also used by * external plugins to check if a connection has been established. */ public function is_connected() { return $this->account->is_stripe_connected(); } /** * Checks if the account has not completed onboarding due to users abandoning the process half way. * Also used by WC Core to complete the task "Set up WooPayments". * Called directly by WooCommerce Core. * * @return bool */ public function is_account_partially_onboarded(): bool { return $this->account->is_stripe_connected() && ! $this->account->is_details_submitted(); } /** * Returns the URL of the configuration screen for this gateway, for use in internal links. * Called directly by WooCommerce Core. * * @return string URL of the configuration screen for this gateway */ public static function get_settings_url() { return WC_Payments_Admin_Settings::get_settings_url(); } /** * Text provided to users during onboarding setup. * Called directly by WooCommerce Core. * * @return string */ public function get_setup_help_text() { return __( 'Next we’ll ask you to share a few details about your business to create your account.', 'woocommerce-payments' ); } /** * Get the connection URL. * Called directly by WooCommerce Core. * * @return string Connection URL. */ public function get_connection_url() { // If we have an account, `Set up` will be shown instead of `Connect`. if ( $this->is_connected() ) { return ''; } // Note: Payments Task is not a very accurate from value, but it is the best we can do, for now. return html_entity_decode( WC_Payments_Account::get_connect_url( WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_TASK ) ); } /** * Add a url to the admin order page that links directly to the transactions detail view for authorized intent statuses. * * Called directly by WooCommerce Core. * * @since 1.4.0 * * @param WC_Order $order The context passed into this function when the user view the order details page in WordPress admin. * @return string */ public function get_transaction_url( $order ) { $intent_status = $this->order_service->get_intention_status_for_order( $order ); if ( ! in_array( $intent_status, Intent_Status::AUTHORIZED_STATUSES, true ) ) { return ''; } $intent_id = $this->order_service->get_intent_id_for_order( $order ); $charge_id = $this->order_service->get_charge_id_for_order( $order ); return WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id ); } /** * Returns true if the gateway needs additional configuration, false if it's ready to use. * * @see WC_Payment_Gateway::needs_setup * @return bool */ public function needs_setup() { if ( ! $this->is_connected() ) { return true; } $account_status = $this->account->get_account_status_data(); return parent::needs_setup() || ! empty( $account_status['error'] ) || ! $account_status['paymentsEnabled']; } /** * Returns whether a store that is not in test mode needs to set https * in the checkout * * @return boolean True if needs to set up forced ssl in checkout or https */ public function needs_https_setup() { return ! WC_Payments::mode()->is_test() && ! wc_checkout_is_https(); } /** * Checks if the gateway is enabled, and also if it's configured enough to accept payments from customers. * * Use parent method value alongside other business rules to make the decision. * * @return bool Whether the gateway is enabled and ready to accept payments. */ public function is_available() { if ( ! WC_Payments::get_gateway()->is_enabled() ) { return false; } $payment_method_id = $this->payment_method->get_id(); $processing_payment_method = $this->payment_methods[ $payment_method_id ]; if ( ! $processing_payment_method->is_enabled_at_checkout( $this->get_account_country() ) ) { return false; } $payment_method_statuses = $this->get_upe_enabled_payment_method_statuses(); $stripe_key = $this->get_payment_method_capability_key_map()[ $payment_method_id ] ?? null; $is_payment_method_active = array_key_exists( $stripe_key, $payment_method_statuses ) && 'active' === $payment_method_statuses[ $stripe_key ]['status']; if ( false === $is_payment_method_active ) { return false; } if ( ! $this->payment_method->is_reusable() && is_add_payment_method_page() ) { return false; } // Disable the gateway if using live mode without HTTPS set up or the currency is not // available in the country of the account. if ( $this->needs_https_setup() || ! $this->is_available_for_current_currency() ) { return false; } if ( in_array( $this->payment_method->get_id(), [ Payment_Method::AFTERPAY, Payment_Method::AFFIRM ], true ) && is_wc_endpoint_url( 'order-pay' ) ) { $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) ); $order = is_a( $order, 'WC_Order' ) ? $order : null; if ( $order ) { $address = $this->retrieve_usable_shipping_data_from_order( $order ); if ( ! $address ) { return false; } if ( Payment_Method::AFFIRM === $this->payment_method->get_id() && empty( $address['name'] ) ) { return false; } } } // Disable the gateway if it should not be displayed on the checkout page. $is_gateway_enabled = in_array( $this->stripe_id, $this->get_payment_method_ids_enabled_at_checkout(), true ) ? true : false; if ( ! $is_gateway_enabled ) { return false; } return parent::is_available() && ! $this->needs_setup(); } /** * Overrides the parent method by adding an additional check to see if the tokens list is empty. * If it is, the method avoids displaying the HTML element with an empty line to maintain a clean user interface and remove unnecessary space. * * @return void */ public function saved_payment_methods() { if ( empty( $this->get_tokens() ) ) { return; } parent::saved_payment_methods(); } /** * Checks if the setting to allow the user to save cards is enabled. * * @return bool Whether the setting to allow saved cards is enabled or not. */ public function is_saved_cards_enabled() { return 'yes' === $this->get_option( 'saved_cards' ); } /** * Checks if the setting to show the payment request buttons is enabled. * * @return bool Whether the setting to show the payment request buttons is enabled or not. */ public function is_payment_request_enabled() { $google_pay_gateway = WC_Payments::get_payment_gateway_by_id( \WCPay\PaymentMethods\Configs\Definitions\GooglePayDefinition::get_id() ); if ( $google_pay_gateway && $google_pay_gateway->is_enabled() ) { return true; } // Fallback, just in case. $apple_pay_gateway = WC_Payments::get_payment_gateway_by_id( \WCPay\PaymentMethods\Configs\Definitions\ApplePayDefinition::get_id() ); if ( $apple_pay_gateway && $apple_pay_gateway->is_enabled() ) { return true; } return false; } /** * Check if account is eligible for card present. * * @return bool */ public function is_card_present_eligible(): bool { try { return $this->account->is_card_present_eligible(); } catch ( Exception $e ) { Logger::error( 'Failed to get account card present eligible. ' . $e ); return false; } } /** * Check if account is eligible for card testing protection. * * @return bool */ public function is_card_testing_protection_eligible(): bool { try { return $this->account->is_card_testing_protection_eligible(); } catch ( Exception $e ) { Logger::error( 'Failed to get account card testing protection eligible. ' . $e ); return false; } } /** * Checks if the account country is compatible with the current currency. * * @return bool Whether the currency is supported in the country set in the account. */ public function is_available_for_current_currency() { $supported_currencies = $this->account->get_account_customer_supported_currencies(); $current_currency = strtolower( get_woocommerce_currency() ); if ( count( $supported_currencies ) === 0 ) { // If we don't have info related to the supported currencies // of the country, we won't disable the gateway. return true; } return in_array( $current_currency, $supported_currencies, true ); } /** * Admin Panel Options. */ public function admin_options() { // Add notices to the WooPayments settings page. do_action( 'woocommerce_woocommerce_payments_admin_notices' ); $this->output_payments_settings_screen(); } /** * Generates markup for the settings screen. */ public function output_payments_settings_screen() { // hiding the save button because the react container has its own. global $hide_save_button; $hide_save_button = true; // to ensure the WC Core script that prompts the "unsaved changes" dialog appears. // that script interferes with merchant actions. wp_dequeue_script( 'woocommerce_settings' ); $method_title = 'WooPayments'; $return_url = 'admin.php?page=wc-settings&tab=checkout'; if ( ! empty( $_GET['method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended // Override the title and return url for method-specific pages in WooPayments settings. $method = sanitize_text_field( wp_unslash( $_GET['method'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $return_url = 'admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments'; switch ( $method ) { case 'payment_request': $method_title = 'Apple Pay / Google Pay'; break; case 'woopay': $method_title = 'WooPay'; break; case 'amazon_pay': $method_title = 'Amazon Pay'; break; } } if ( function_exists( 'wc_back_header' ) ) { wc_back_header( $method_title, __( 'Return to payments', 'woocommerce-payments' ), $return_url ); } else { // Until the wc_back_header function is available (WC Core 9.9) use the current available version. echo '<h2>'; echo esc_html( $method_title ); wc_back_link( __( 'Return to payments', 'woocommerce-payments' ), $return_url ); echo '</h2>'; } if ( ! empty( $_GET['method'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?> <div id="wcpay-express-checkout-settings-container" data-method-id="<?php echo esc_attr( sanitize_text_field( wp_unslash( $_GET['method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>" ></div> <?php else : ?> <div id="wcpay-account-settings-container"></div> <?php endif; } /** * Displays the save to account checkbox. * * @param bool $force_checked True if the checkbox must be forced to "checked" state (and invisible). */ public function save_payment_method_checkbox( $force_checked = false ) { $id = 'wc-' . $this->id . '-new-payment-method'; $should_hide = $force_checked || $this->should_use_stripe_platform_on_checkout_page(); ?> <div <?php echo $should_hide ? 'style="display:none;"' : ''; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped */ ?>> <p class="form-row woocommerce-SavedPaymentMethods-saveNew"> <input id="<?php echo esc_attr( $id ); ?>" name="<?php echo esc_attr( $id ); ?>" type="checkbox" value="true" style="width:auto; vertical-align: middle; position: relative; bottom: 1px;" <?php echo $force_checked ? 'checked' : ''; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped */ ?> /> <label for="<?php echo esc_attr( $id ); ?>" style="display:inline;"> <?php echo esc_html( apply_filters( 'wc_payments_save_to_account_text', __( 'Save payment information to my account for future purchases.', 'woocommerce-payments' ) ) ); ?> </label> </p> </div> <?php } /** * Whether we should use the platform account to initialize Stripe on the checkout page. * * @return bool */ public function should_use_stripe_platform_on_checkout_page() { if ( 'card' !== $this->stripe_id ) { return false; } if ( WC_Payments_Features::is_woopay_eligible() && 'yes' === $this->get_option( 'platform_checkout', 'no' ) && ( is_checkout() || has_block( 'woocommerce/checkout' ) ) && ! is_wc_endpoint_url( 'order-pay' ) && WC()->cart instanceof WC_Cart && ! WC()->cart->is_empty() && WC()->cart->needs_payment() ) { return true; } return false; } /** * Process the payment for a given order. * * @param int $order_id Order ID to process the payment for. * * @return array|null An array with result of payment and redirect URL, or nothing. * @throws Exception Error processing the payment. */ public function process_payment( $order_id ) { $order = wc_get_order( $order_id ); try { if ( 20 < strlen( $order->get_billing_phone() ) ) { throw new Invalid_Phone_Number_Exception( __( 'Invalid phone number.', 'woocommerce-payments' ), 'invalid_phone_number' ); } // Check if session exists and we're currently not processing a WooPay request before instantiating `Fraud_Prevention_Service`. if ( WC()->session && ! apply_filters( 'wcpay_is_woopay_store_api_request', false ) ) { $fraud_prevention_service = Fraud_Prevention_Service::get_instance(); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( $fraud_prevention_service->is_enabled() && ! $fraud_prevention_service->verify_token( $_POST['wcpay-fraud-prevention-token'] ?? null ) ) { throw new Fraud_Prevention_Enabled_Exception( __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-payments' ), 'fraud_prevention_enabled' ); } } if ( $this->failed_transaction_rate_limiter->is_limited() ) { throw new Rate_Limiter_Enabled_Exception( __( 'Your payment was not processed.', 'woocommerce-payments' ), 'rate_limiter_enabled' ); } // The request is a preflight check from WooPay. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['is-woopay-preflight-check'] ) ) { // Set the order status to "pending payment". $order->update_status( 'pending' ); // Bail out with success so we don't process the payment now, // but still let WooPay continue with the payment processing. return [ 'result' => 'success', 'redirect' => '', ]; } $check_session_order = $this->duplicate_payment_prevention_service->check_against_session_processing_order( $order ); if ( is_array( $check_session_order ) ) { return $check_session_order; } $this->duplicate_payment_prevention_service->maybe_update_session_processing_order( $order_id ); $check_existing_intention = $this->duplicate_payment_prevention_service->check_payment_intent_attached_to_order_succeeded( $order ); if ( is_array( $check_existing_intention ) ) { return $check_existing_intention; } $payment_information = $this->prepare_payment_information( $order ); return $this->process_payment_for_order( WC()->cart, $payment_information ); } catch ( Exception $e ) { // Log the exception. Logger::exception( 'Error occurred during the payment process.', $e ); // We set this variable to be used in following checks. $blocked_by_fraud_rules = $this->is_blocked_by_fraud_rules( $e ); do_action( 'woocommerce_payments_order_failed', $order, $e ); /** * TODO: Determine how to do this update with Order_Service. * It seems that the status only needs to change in certain instances, and within those instances the intent * information is not added to the order, as shown by tests. */ if ( $e instanceof Process_Payment_Exception && 'duplicate_payment_amount_mismatch' === $e->get_error_code() ) { $order->update_status( Order_Status::FAILED, $e->getMessage() ); } elseif ( ! $blocked_by_fraud_rules && ( empty( $payment_information ) || ! $payment_information->is_changing_payment_method_for_subscription() ) ) { $order->update_status( Order_Status::FAILED ); } if ( $e instanceof API_Exception && $this->should_bump_rate_limiter( $e->get_error_code() ) ) { $this->failed_transaction_rate_limiter->bump(); } if ( $blocked_by_fraud_rules ) { $this->order_service->mark_order_blocked_for_fraud( $order, '', Intent_Status::CANCELED ); } elseif ( ! empty( $payment_information ) ) { /** * TODO: Move the contents of this else into the Order_Service. */ /* translators: %1: the failed payment amount, %2: error message */ $error_message = __( 'A payment of %1$s <strong>failed</strong> to complete with the following message: <code>%2$s</code>.', 'woocommerce-payments' ); $error_details = esc_html( rtrim( $e->getMessage(), '.' ) ); if ( $e instanceof API_Merchant_Exception ) { $error_details = $error_details . '. ' . esc_html( rtrim( $e->get_merchant_message(), '.' ) ); } if ( $e instanceof API_Exception && 'card_error' === $e->get_error_type() ) { // If the payment failed with a 'card_error' API exception, initialize the fraud meta box // type with ALLOW, because fraud checks are passed, and the payment returned a "card error". $this->order_service->set_fraud_meta_box_type_for_order( $order, Fraud_Meta_Box_Type::ALLOW ); if ( 'incorrect_zip' === $e->get_error_code() ) { /* translators: %1: the failed payment amount, %2: error message */ $error_message = __( 'A payment of %1$s <strong>failed</strong>. %2$s', 'woocommerce-payments' ); $error_details = __( 'We couldn’t verify the postal code in the billing address. If the issue persists, suggest the customer to reach out to the card issuing bank.', 'woocommerce-payments' ); } } $note = sprintf( WC_Payments_Utils::esc_interpolated_html( $error_message, [ 'strong' => '<strong>', 'code' => '<code>', ] ), WC_Payments_Explicit_Price_Formatter::get_explicit_price( wc_price( $order->get_total(), [ 'currency' => $order->get_currency() ] ), $order ), $error_details ); $order->add_order_note( $note ); } if ( $e instanceof Process_Payment_Exception && 'rate_limiter_enabled' === $e->get_error_code() ) { /** * TODO: Move the contents of this into the Order_Service. */ $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the failed payment amount */ __( 'A payment of %1$s <strong>failed</strong> to complete because of too many failed transactions. A rate limiter was enabled for the user to prevent more attempts temporarily.', 'woocommerce-payments' ), [ 'strong' => '<strong>', ] ), WC_Payments_Explicit_Price_Formatter::get_explicit_price( wc_price( $order->get_total(), [ 'currency' => $order->get_currency() ] ), $order ) ); $order->add_order_note( $note ); } // This allows WC to check if WP_DEBUG mode is enabled before returning previous Exception and expose Exception class name to frontend. add_filter( 'woocommerce_return_previous_exceptions', '__return_true' ); wc_add_notice( wp_strip_all_tags( WC_Payments_Utils::get_filtered_error_message( $e, $blocked_by_fraud_rules ) ), 'error' ); do_action( 'wcpay_update_payment_result_on_error', $e, $order ); return [ 'result' => 'fail', 'redirect' => '', ]; } } /** * Prepares the payment information object. * * @param WC_Order $order The order whose payment will be processed. * @return Payment_Information An object, which describes the payment. */ protected function prepare_payment_information( $order ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $payment_information = Payment_Information::from_payment_request( $_POST, $order, Payment_Type::SINGLE(), Payment_Initiated_By::CUSTOMER(), $this->get_capture_type(), $this->get_payment_method_to_use_for_intent() ); $payment_information = $this->maybe_prepare_subscription_payment_information( $payment_information, $order->get_id() ); if ( ! empty( $_POST[ 'wc-' . static::GATEWAY_ID . '-new-payment-method' ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing // During normal orders the payment method is saved when the customer enters a new one and chooses to save it. $payment_information->must_save_payment_method_to_store(); } if ( $this->woopay_util->should_save_platform_customer() ) { do_action( 'woocommerce_payments_save_user_in_woopay' ); $payment_information->must_save_payment_method_to_platform(); } return $payment_information; } /** * Update the customer details with the incoming order data, in a CRON job. * * @param \WC_Order $order WC order id. * @param string $customer_id The customer id to update details for. * @param bool $is_test_mode Whether to run the CRON job in test mode. * @param bool $is_woopay Whether CRON job was queued from WooPay. */ public function update_customer_with_order_data( $order, $customer_id, $is_test_mode = false, $is_woopay = false ) { // Since this CRON job may have been created in test_mode, when the CRON job runs, it // may lose the test_mode context. So, instead, we pass that context when creating // the CRON job and apply the context here. $apply_test_mode_context = function () use ( $is_test_mode ) { return $is_test_mode; }; add_filter( 'wcpay_test_mode', $apply_test_mode_context ); $user = $order->get_user(); if ( false === $user ) { $user = wp_get_current_user(); } // Since this function will run in a CRON job, "wp_get_current_user()" will default // to user with ID of 0. So, instead, we replace it with the user from the $order, // when updating a WooPay user. $apply_order_user_email = function ( $params ) use ( $user, $is_woopay ) { if ( $is_woopay ) { $params['email'] = $user->user_email; } return $params; }; add_filter( 'wcpay_api_request_params', $apply_order_user_email, 20, 1 ); // Update the existing customer with the current order details. $customer_data = WC_Payments_Customer_Service::map_customer_data( $order, new WC_Customer( $user->ID ) ); $this->customer_service->update_customer_for_user( $customer_id, $user, $customer_data ); } /** * Sets up a handler to add error details to the payment result. * Registers an action to handle 'wcpay_update_payment_result_on_error', * using the payment result object from 'woocommerce_rest_checkout_process_payment_with_context'. * * @param PaymentContext $context The payment context. * @param PaymentResult $result The payment result, passed by reference. */ public function setup_payment_error_handler( PaymentContext $context, PaymentResult &$result ) { add_action( 'wcpay_update_payment_result_on_error', function ( $error ) use ( &$result ) { $result->set_payment_details( array_merge( $result->payment_details, [ 'errorMessage' => wp_strip_all_tags( $error->getMessage() ) ] ) ); } ); } /** * Manages customer details held on WCPay server for WordPress user associated with an order. * * @param WC_Order $order WC Order object. * @param array $options Additional options to apply. * * @return array First element is the new or updated WordPress user, the second element is the WCPay customer ID. */ protected function manage_customer_details_for_order( $order, $options = [] ) { $user = $order->get_user(); if ( false === $user ) { $user = wp_get_current_user(); } // Determine the customer making the payment, create one if we don't have one already. $customer_id = $this->customer_service->get_customer_id_by_user_id( $user->ID ); if ( null === $customer_id ) { $customer_data = WC_Payments_Customer_Service::map_customer_data( $order, new WC_Customer( $user->ID ) ); // Create a new customer. $customer_id = $this->customer_service->create_customer_for_user( $user, $customer_data ); } else { // Update the customer with order data async. $this->update_customer_with_order_data( $order, $customer_id, WC_Payments::mode()->is_test(), $options['is_woopay'] ?? false ); } return [ $user, $customer_id ]; } /** * Update the saved payment method information with checkout values, in a CRON job. * * @param string $payment_method The payment method to update. * @param int $order_id WC order id. * @param bool $is_test_mode Whether to run the CRON job in test mode. */ public function update_saved_payment_method( $payment_method, $order_id, $is_test_mode = false ) { // Since this CRON job may have been created in test_mode, when the CRON job runs, it // may lose the test_mode context. So, instead, we pass that context when creating // the CRON job and apply the context here. $apply_test_mode_context = function () use ( $is_test_mode ) { return $is_test_mode; }; add_filter( 'wcpay_test_mode', $apply_test_mode_context ); $order = wc_get_order( $order_id ); try { $this->customer_service->update_payment_method_with_billing_details_from_order( $payment_method, $order ); } catch ( Exception $e ) { // If updating the payment method fails, log the error message. Logger::log( 'Error when updating saved payment method: ' . $e->getMessage() ); } } /** * Process the payment for a given order. * * @param WC_Cart|null $cart Cart. * @param WCPay\Payment_Information $payment_information Payment info. * @param bool $scheduled_subscription_payment Used to determinate is scheduled subscription payment to add more fields into API request. * * @return array|null An array with result of payment and redirect URL, or nothing. * @throws API_Exception * @throws Exception When amount too small. * @throws Invalid_Address_Exception * @throws Order_Not_Found_Exception * @throws Order_ID_Mismatch_Exception When the payment intent could not be authenticated. * @throws \WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception When request class filter filed to extend request class because of incompatibility. * @throws \WCPay\Core\Exceptions\Server\Request\Immutable_Parameter_Exception When immutable parameter gets changed in request class. * @throws \WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception When you send incorrect request value via setters. */ public function process_payment_for_order( $cart, $payment_information, $scheduled_subscription_payment = false ) { $order = $payment_information->get_order(); $save_payment_method_to_store = $payment_information->should_save_payment_method_to_store(); $is_changing_payment_method_for_subscription = $payment_information->is_changing_payment_method_for_subscription(); $order_id = $order->get_id(); $amount = $order->get_total(); $metadata = $this->get_metadata_from_order( $order, $payment_information->get_payment_type() ); $customer_details_options = [ 'is_woopay' => filter_var( $metadata['paid_on_woopay'] ?? false, FILTER_VALIDATE_BOOLEAN ), ]; if ( $payment_information->get_customer_id() ) { $user = $order->get_user(); $customer_id = $payment_information->get_customer_id(); } else { list( $user, $customer_id ) = $this->manage_customer_details_for_order( $order, $customer_details_options ); } $intent_failed = false; $payment_needed = $amount > 0; // Make sure that we attach the payment method and the customer ID to the order meta data. // Note: For confirmation tokens (ctoken_*), we don't store them here as they are not valid // payment method IDs. The real payment method ID will be stored after intent creation. $payment_method = $payment_information->get_payment_method(); if ( ! $payment_information->is_using_confirmation_token() ) { $this->order_service->set_payment_method_id_for_order( $order, $payment_method ); } $this->order_service->set_customer_id_for_order( $order, $customer_id ); $order->update_meta_data( WC_Payments_Order_Service::WCPAY_MODE_META_KEY, WC_Payments::mode()->is_test() ? Order_Mode::TEST : Order_Mode::PRODUCTION ); // If an error happened during the payment setup in the client it will be saved in the payment information so we can throw // the error here and follow the standard failed order flow. $error = $payment_information->get_error(); if ( ! is_null( $error ) ) { throw new \Exception( $error->get_error_message() ); } // In case amount is 0 and we're not saving the payment method, we won't be using intents and can confirm the order payment. if ( apply_filters( 'wcpay_confirm_without_payment_intent', ! $payment_needed && ! $save_payment_method_to_store ) ) { $order->payment_complete(); if ( $payment_information->is_using_saved_payment_method() ) { // We need to make sure the saved payment method is saved to the order so we can // charge the payment method for a future payment. $this->add_token_to_order( $order, $payment_information->get_payment_token() ); // If we are not hitting the API for the intent, we need to update the saved payment method ourselves. $this->action_scheduler_service->schedule_job( time(), self::UPDATE_SAVED_PAYMENT_METHOD, [ 'payment_method' => $payment_information->get_payment_method(), 'order_id' => $order->get_id(), 'is_test_mode' => WC_Payments::mode()->is_test(), ] ); } if ( $is_changing_payment_method_for_subscription && $payment_information->is_using_saved_payment_method() ) { $payment_token = $payment_information->get_payment_token(); $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the last 4 digit of the credit card */ __( 'Payment method is changed to: <strong>Credit card ending in %1$s</strong>.', 'woocommerce-payments' ), [ 'strong' => '<strong>', ] ), $payment_token instanceof WC_Payment_Token_CC ? $payment_token->get_last4() : '----' ); $order->add_order_note( $note ); do_action( 'woocommerce_payments_changed_subscription_payment_method', $order, $payment_token ); } $order->set_payment_method_title( __( 'Credit / Debit Cards', 'woocommerce-payments' ) ); $order->save(); return [ 'result' => 'success', 'redirect' => $this->get_return_url( $order ), ]; } if ( $payment_needed ) { $converted_amount = WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ); $currency = strtolower( $order->get_currency() ); // Try catching the error without reaching the API. $minimum_amount = WC_Payments_Utils::get_cached_minimum_amount( $currency ); if ( $minimum_amount > $converted_amount ) { $e = new Amount_Too_Small_Exception( 'Amount too small', $minimum_amount, $currency, 400 ); throw new Exception( WC_Payments_Utils::get_filtered_error_message( $e ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $woopay_intent_id = WooPay_Utilities::sanitize_intent_id( wp_unslash( $_POST['platform-checkout-intent'] ?? '' ) ); // Initializing the intent variable here to ensure we don't try to use an undeclared // variable later. $intent = null; if ( ! empty( $woopay_intent_id ) ) { // If the intent is included in the request use that intent. $request = Get_Intention::create( $woopay_intent_id ); $request->set_hook_args( $order ); $intent = $request->send(); $intent_meta_order_id_raw = $intent->get_metadata()['order_id'] ?? ''; $intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0; if ( $intent_meta_order_id !== $order_id ) { throw new Order_ID_Mismatch_Exception( sprintf( /* translators: %s: metadata. We do not need to translate WooPayMeta */ esc_html( __( 'We\'re not able to process this payment. Please try again later. WooPayMeta: intent_meta_order_id: %1$s, order_id: %2$s', 'woocommerce-payments' ) ), esc_attr( $intent_meta_order_id ), esc_attr( $order_id ) ), 'order_id_mismatch' ); } } if ( empty( $intent ) ) { $payment_methods = $this->get_payment_method_types( $payment_information ); $request = Create_And_Confirm_Intention::create(); $request->set_amount( $converted_amount ); $request->set_currency_code( $currency ); $payment_credential = $payment_information->get_payment_method(); if ( $payment_information->is_using_confirmation_token() ) { $request->set_confirmation_token( $payment_credential ); } else { $request->set_payment_method( $payment_credential ); } $request->set_customer( $customer_id ); $request->set_capture_method( $payment_information->is_using_manual_capture() ); $request->set_metadata( $metadata ); $request->set_level3( $this->get_level3_data_from_order( $order ) ); $request->set_off_session( $payment_information->is_merchant_initiated() ); $request->set_payment_methods( $payment_methods ); $request->set_cvc_confirmation( $payment_information->get_cvc_confirmation() ); $request->set_hook_args( $payment_information ); if ( $payment_information->is_using_saved_payment_method() ) { $billing_details = $this->order_service->get_billing_data_from_order( $order ); $is_legacy_card_object = (bool) preg_match( '/^(card_|src_)/', $payment_information->get_payment_method() ); // Not updating billing details for legacy card objects because they have a different structure and are no longer supported. if ( ! empty( $billing_details ) && ! $is_legacy_card_object ) { $request->set_payment_method_update_data( [ 'billing_details' => $billing_details ] ); } } // Add specific payment method parameters to the request. $this->modify_create_intent_parameters_when_processing_payment( $request, $payment_information, $order ); // The below if-statement ensures the support for UPE payment methods. if ( $this->upe_needs_redirection( $payment_methods ) ) { $request->set_return_url( wp_sanitize_redirect( esc_url_raw( add_query_arg( [ 'wc_payment_method' => self::GATEWAY_ID, '_wpnonce' => wp_create_nonce( 'wcpay_process_redirect_order_nonce' ), ], $this->get_return_url( $order ) ) ) ) ); } // Make sure that setting fingerprint is performed after setting metadata because metadata will override any values you set before for metadata param. $request->set_fingerprint( $payment_information->get_fingerprint() ); if ( $save_payment_method_to_store ) { // Only set setup_future_usage for reusable payment methods. // Non-reusable payment methods (e.g., iDEAL) will be used for manual renewals and don't support setup_future_usage. if ( $this->payment_method->is_reusable() ) { $request->setup_future_usage(); } } if ( $scheduled_subscription_payment ) { $mandate = $this->get_mandate_param_for_renewal_order( $order ); if ( $mandate ) { $request->set_mandate( $mandate ); } } // For Stripe Link & SEPA, we must create mandate to acknowledge that terms have been shown to customer. if ( $this->is_mandate_data_required() ) { $request->set_mandate_data( $this->get_mandate_data() ); } /** @var WC_Payments_API_Payment_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort $intent = $request->send(); } $intent_id = $intent->get_id(); $status = $intent->get_status(); $charge = $intent->get_charge(); $charge_id = $charge ? $charge->get_id() : null; $client_secret = $intent->get_client_secret(); $currency = $intent->get_currency(); $next_action = $intent->get_next_action(); $processing = $intent->get_processing(); // We update the payment method ID server side when it's necessary to clone payment methods, // for example when saving a payment method to a platform customer account. When this happens // we need to make sure the payment method on the order matches the one on the merchant account // not the one on the platform account. The payment method ID is updated on the order further // down. $payment_method = $intent->get_payment_method_id() ?? $payment_method; if ( Intent_Status::REQUIRES_ACTION === $status && $payment_information->is_merchant_initiated() ) { // Allow 3rd-party to trigger some action if needed. do_action( 'woocommerce_woocommerce_payments_payment_requires_action', $order, $intent_id, $payment_method, $customer_id, $charge_id, $currency ); $this->order_service->mark_payment_failed( $order, $intent_id, $status, $charge_id ); } } else { // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $woopay_intent_id = WooPay_Utilities::sanitize_intent_id( wp_unslash( $_POST['platform-checkout-intent'] ?? '' ) ); if ( ! empty( $woopay_intent_id ) ) { // If the setup intent is included in the request use that intent. $setup_intent_request = Get_Setup_Intention::create( $woopay_intent_id ); /** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort $intent = $setup_intent_request->send(); $intent_metadata = $intent->get_metadata(); $intent_meta_order_id_raw = $intent_metadata['order_id'] ?? ''; $intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0; if ( $intent_meta_order_id !== $order_id ) { throw new Order_ID_Mismatch_Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-payments' ), 'order_id_mismatch' ); } } else { $save_user_in_woopay = false; if ( $this->woopay_util->should_save_platform_customer() ) { $save_user_in_woopay = true; $metadata_from_order = apply_filters( 'wcpay_metadata_from_order', [ 'customer_email' => $order->get_billing_email(), ], $order ); $metadata = array_merge( (array) $metadata_from_order, (array) $metadata ); // prioritize metadata from mobile app. do_action( 'woocommerce_payments_save_user_in_woopay' ); } // For $0 orders, we need to save the payment method using a setup intent. $request = Create_And_Confirm_Setup_Intention::create(); $request->set_customer( $customer_id ); // Setting the credential based on what was provided. $payment_credential = $payment_information->get_payment_method(); if ( $payment_information->is_using_confirmation_token() ) { $request->set_confirmation_token( $payment_credential ); } else { $request->set_payment_method( $payment_credential ); } $request->set_metadata( $metadata ); $request->assign_hook( 'wcpay_create_and_confirm_setup_intention_request' ); $request->set_hook_args( $payment_information, false, $save_user_in_woopay ); if ( Payment_Method::CARD === $this->get_selected_stripe_payment_type_id() && in_array( Payment_Method::LINK, $this->get_upe_enabled_payment_method_ids(), true ) ) { $request->set_payment_method_types( $this->get_payment_method_types( $payment_information ) ); $request->set_mandate_data( $this->get_mandate_data() ); } /** @var WC_Payments_API_Setup_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort $intent = $request->send(); } $intent_id = $intent->get_id(); $status = $intent->get_status(); $charge_id = ''; $charge = null; $client_secret = $intent->get_client_secret(); $currency = $order->get_currency(); $next_action = $intent->get_next_action(); $processing = []; } $is_offline_payment_method = $payment_information->is_offline_payment_method(); if ( ! empty( $intent ) ) { if ( ! $intent->is_authorized() ) { $intent_failed = true; } // For confirmation tokens, we now have access to the real payment method ID from the intent. // Store it so that other code can use it for displaying payment method details. if ( $payment_information->is_using_confirmation_token() ) { $intent_payment_method_id = $intent->get_payment_method_id(); if ( $intent_payment_method_id ) { $this->order_service->set_payment_method_id_for_order( $order, $intent_payment_method_id ); } } if ( $save_payment_method_to_store && ! $intent_failed ) { try { $token = null; // For WooPay checkouts, we may provide a platform payment method from `$payment_information`, but we need // to return a connected payment method. So we should always retrieve the payment method from the intent. $payment_method_id = $intent->get_payment_method_id(); // Handle orders that are paid via WooPay and contain subscriptions. if ( $order->get_meta( 'is_woopay' ) && function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order ) ) { $customer_tokens = WC_Payment_Tokens::get_customer_tokens( $order->get_user_id(), self::GATEWAY_ID ); // Use the existing token if we already have one for the incoming payment method. foreach ( $customer_tokens as $saved_token ) { if ( $saved_token->get_token() === $payment_method_id ) { $token = $saved_token; break; } } } // Store a new token if we're not paying for a subscription in WooPay, // or if we are, but we didn't find a stored token for the selected payment method. if ( empty( $token ) ) { $token = $this->token_service->add_payment_method_to_user( $payment_method_id, $user ); } $payment_information->set_token( $token ); } catch ( Exception $e ) { // If saving the token fails, log the error message but catch the error to avoid crashing the checkout flow. Logger::log( 'Error when saving payment method: ' . $e->getMessage() ); } } if ( $payment_information->is_using_saved_payment_method() ) { $token = $payment_information->get_payment_token(); $this->add_token_to_order( $order, $token ); if ( $order->get_meta( '_woopay_has_subscription' ) ) { $token->update_meta_data( 'is_attached_to_subscription', '1' ); $token->save_meta_data(); } } if ( Intent_Status::REQUIRES_ACTION === $status ) { $next_action_type = $next_action['type'] ?? null; if ( 'redirect_to_url' === $next_action_type && ! empty( $next_action[ $next_action_type ]['url'] ) ) { $response = [ 'result' => 'success', 'redirect' => $next_action[ $next_action_type ]['url'], ]; } elseif ( 'multibanco_display_details' === $next_action_type ) { $this->order_service->attach_multibanco_info_to_order( $order, $next_action[ $next_action_type ]['reference'], $next_action[ $next_action_type ]['entity'], $next_action[ $next_action_type ]['hosted_voucher_url'], $next_action[ $next_action_type ]['expires_at'] ); } else { $response = [ 'result' => 'success', // Include a new nonce for update_order_status to ensure the update order // status call works when a guest user creates an account during checkout. 'redirect' => sprintf( '#wcpay-confirm-%s:%s:%s:%s', $payment_needed ? 'pi' : 'si', $order_id, $client_secret, wp_create_nonce( 'wcpay_update_order_status_nonce' ), ), // Include the payment method ID so the Blocks integration can save cards. 'payment_method' => $payment_information->get_payment_method(), ]; } } elseif ( $this->is_changing_payment_method_for_subscription() ) { // Only attempt to use WC_Subscriptions_Change_Payment_Gateway if it exists. if ( class_exists( 'WC_Subscriptions_Change_Payment_Gateway' ) ) { // Update the payment method for subscription if the payment intent is not requiring action. WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $order, $this->id ); } // Because this new payment does not require action/confirmation, remove this filter so that WC_Subscriptions_Change_Payment_Gateway proceeds to update all subscriptions if flagged. remove_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', [ $this, 'update_payment_method_for_subscriptions' ], 10 ); } } $allow_update_on_success = $this->is_changing_payment_method_for_subscription() || $this->is_subscription_item_in_cart(); $this->order_service->attach_intent_info_to_order( $order, $intent, $allow_update_on_success ); $this->attach_exchange_info_to_order( $order, $charge_id ); if ( Intent_Status::SUCCEEDED === $status || ( Intent_Status::REQUIRES_ACTION === $status && $is_offline_payment_method ) ) { $this->duplicate_payment_prevention_service->remove_session_processing_order( $order->get_id() ); } $this->order_service->update_order_status_from_intent( $order, $intent ); $this->order_service->attach_transaction_fee_to_order( $order, $charge ); $this->maybe_add_customer_notification_note( $order, $processing ); // Extract payment method details for setting the payment method title. if ( $payment_needed ) { $charge = $intent ? $intent->get_charge() : null; $payment_method_details = $charge ? $charge->get_payment_method_details() : []; // Get payment method type from charge details (actual payment method used). // Fallback to intent's method only if charge details not available. $payment_method_type = $payment_method_details['type'] ?? null; if ( ! $payment_method_type && $intent ) { $payment_method_type = $intent->get_payment_method_type(); } if ( 'card' === $payment_method_type && isset( $payment_method_details['card']['last4'] ) ) { $order->add_meta_data( 'last4', $payment_method_details['card']['last4'], true ); if ( isset( $payment_method_details['card']['brand'] ) ) { $order->add_meta_data( '_card_brand', $payment_method_details['card']['brand'], true ); } $order->save_meta_data(); } } else { $payment_method_details = false; $token = $payment_information->is_using_saved_payment_method() ? $payment_information->get_payment_token() : null; $payment_method_type = $token ? $this->get_payment_method_type_for_setup_intent( $intent, $token ) : null; } // ensuring the payment method title is set before any early return paths to avoid incomplete order data. $this->set_payment_method_title_for_order( $order, $payment_method_type, $payment_method_details ); if ( isset( $status ) && Intent_Status::REQUIRES_ACTION === $status && $this->is_changing_payment_method_for_subscription() ) { // Because we're filtering woocommerce_subscriptions_update_payment_via_pay_shortcode, we need to manually set this delayed update all flag here. if ( isset( $_POST['update_all_subscriptions_payment_method'] ) && wc_clean( wp_unslash( $_POST['update_all_subscriptions_payment_method'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $order->update_meta_data( '_delayed_update_payment_method_all', wc_clean( wp_unslash( $_POST['payment_method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $order->save(); } wp_safe_redirect( $response['redirect'] ); exit; } if ( isset( $response ) && ! $is_offline_payment_method ) { return $response; } wc_maybe_reduce_stock_levels( $order_id ); if ( isset( $cart ) ) { $cart->empty_cart(); } return [ 'result' => 'success', 'redirect' => $this->get_return_url( $order ), ]; } /** * Check for a redirect payment method on order received page or setup intent on payment methods page. */ public function maybe_process_upe_redirect() { if ( $this->is_payment_methods_page() ) { // If a payment method was added using UPE, we need to clear the cache and notify the user. if ( $this->is_setup_intent_success_creation_redirection() ) { wc_add_notice( __( 'Payment method successfully added.', 'woocommerce-payments' ) ); $user = wp_get_current_user(); $this->token_service->clear_cached_payment_methods_for_user( $user->ID ); } return; } if ( ! is_order_received_page() ) { return; } $payment_method = isset( $_GET['wc_payment_method'] ) ? wc_clean( wp_unslash( $_GET['wc_payment_method'] ) ) : ''; if ( self::GATEWAY_ID !== $payment_method ) { return; } $is_nonce_valid = check_admin_referer( 'wcpay_process_redirect_order_nonce' ); if ( ! $is_nonce_valid || empty( $_GET['wc_payment_method'] ) ) { return; } if ( ! empty( $_GET['payment_intent_client_secret'] ) ) { $intent_id_from_request = isset( $_GET['payment_intent'] ) ? wc_clean( wp_unslash( $_GET['payment_intent'] ) ) : ''; } elseif ( ! empty( $_GET['setup_intent_client_secret'] ) ) { $intent_id_from_request = isset( $_GET['setup_intent'] ) ? wc_clean( wp_unslash( $_GET['setup_intent'] ) ) : ''; } else { return; } $order_id = absint( get_query_var( 'order-received' ) ); $order_key_from_request = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; $save_payment_method = isset( $_GET['save_payment_method'] ) ? 'yes' === wc_clean( wp_unslash( $_GET['save_payment_method'] ) ) : false; if ( empty( $intent_id_from_request ) || empty( $order_id ) || empty( $order_key_from_request ) ) { return; } $order = wc_get_order( $order_id ); if ( ! is_a( $order, 'WC_Order' ) ) { // the ID of non-existing order was passed in. return; } if ( $order->get_order_key() !== $order_key_from_request ) { // Valid return url should have matching order key. return; } // Perform additional checks for non-zero-amount. For zero-amount orders, we can't compare intents because they are not attached to the order at this stage. // Once https://github.com/Automattic/woocommerce-payments/issues/6575 is closed, this check can be applied for zero-amount orders as well. if ( $order->get_total() > 0 && ! $this->is_proper_intent_used_with_order( $order, $intent_id_from_request ) ) { return; } $this->process_redirect_payment( $order, $intent_id_from_request, $save_payment_method ); } /** * Processes redirect payments. * * @param WC_Order $order The order being processed. * @param string $intent_id The Stripe setup/payment intent ID for the order payment. * @param bool $save_payment_method Boolean representing whether payment method for order should be saved. * * @throws Process_Payment_Exception When the payment intent has an error. */ public function process_redirect_payment( $order, $intent_id, $save_payment_method ) { try { $order_id = $order->get_id(); if ( $order->has_status( [ Order_Status::PROCESSING, Order_Status::COMPLETED, Order_Status::ON_HOLD, ] ) ) { return; } Logger::log( "Begin processing UPE redirect payment for order {$order_id} for the amount of {$order->get_total()}" ); // Get user/customer for order. list( $user, $customer_id ) = $this->manage_customer_details_for_order( $order ); $payment_needed = 0 < $order->get_total(); // Get payment intent to confirm status. if ( $payment_needed ) { $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); /** @var WC_Payments_API_Payment_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort $intent = $request->send(); $client_secret = $intent->get_client_secret(); $status = $intent->get_status(); $charge = $intent->get_charge(); $charge_id = $charge ? $charge->get_id() : null; $currency = $intent->get_currency(); $payment_method_id = $intent->get_payment_method_id(); $payment_method_details = $charge ? $charge->get_payment_method_details() : []; $payment_method_type = $this->get_payment_method_type_from_payment_details( $payment_method_details ); $error = $intent->get_last_payment_error(); // This check applies to payment intents only due to two reasons: // (1) metadata is missed for setup intents. See https://github.com/Automattic/woocommerce-payments/issues/6575. // (2) most issues so far affect only payment intents. $intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : []; $this->validate_order_id_received_vs_intent_meta_order_id( $order, $intent_metadata ); } else { $request = Get_Setup_Intention::create( $intent_id ); /** @var WC_Payments_API_Setup_Intention $intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort $intent = $request->send(); $client_secret = $intent->get_client_secret(); $status = $intent->get_status(); $charge_id = ''; $charge = null; $currency = $order->get_currency(); $payment_method_id = $intent->get_payment_method_id(); $payment_method_details = false; $payment_method_type = $intent->get_payment_method_type(); $error = $intent->get_last_setup_error(); } if ( ! empty( $error ) ) { Logger::log( 'Error when processing payment: ' . $error['message'] ); throw new Process_Payment_Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-payments' ), 'upe_payment_intent_error' ); } else { $payment_method = $this->get_selected_payment_method( $payment_method_type ); if ( ! $payment_method ) { return; } if ( $save_payment_method && $payment_method->is_reusable() ) { try { $token = $payment_method->get_payment_token_for_user( $user, $payment_method_id ); $this->add_token_to_order( $order, $token ); } catch ( Exception $e ) { // If saving the token fails, log the error message but catch the error to avoid crashing the checkout flow. Logger::log( 'Error when saving payment method: ' . $e->getMessage() ); } } $this->order_service->attach_intent_info_to_order( $order, $intent ); $this->attach_exchange_info_to_order( $order, $charge_id ); if ( Intent_Status::SUCCEEDED === $status ) { $this->duplicate_payment_prevention_service->remove_session_processing_order( $order->get_id() ); } $this->set_payment_method_title_for_order( $order, $payment_method_type, $payment_method_details ); $this->order_service->update_order_status_from_intent( $order, $intent ); $this->order_service->attach_transaction_fee_to_order( $order, $charge ); if ( Intent_Status::REQUIRES_ACTION === $status ) { // I don't think this case should be possible, but just in case... $next_action = $intent->get_next_action(); if ( isset( $next_action['type'] ) && 'redirect_to_url' === $next_action['type'] && ! empty( $next_action['redirect_to_url']['url'] ) ) { wp_safe_redirect( $next_action['redirect_to_url']['url'] ); exit; } else { $redirect_url = sprintf( '#wcpay-confirm-%s:%s:%s:%s', $payment_needed ? 'pi' : 'si', $order_id, $client_secret, wp_create_nonce( 'wcpay_update_order_status_nonce' ) ); wp_safe_redirect( $redirect_url ); exit; } } } } catch ( Exception $e ) { Logger::exception( 'Error occurred during the redirect payment process.', $e ); $is_order_id_mismatched_exception = $e instanceof Process_Payment_Exception && self::PROCESS_REDIRECT_ORDER_MISMATCH_ERROR_CODE === $e->get_error_code(); // If the order ID mismatched exception is thrown, do not mark the order as failed. // Because the outcome of the payment intent is for another order, not for the order processed here. if ( ! $is_order_id_mismatched_exception ) { // Confirm our needed variables are set before using them due to there could be a server issue during the get_intent process. $status = $status ?? null; $charge_id = $charge_id ?? null; /* translators: localized exception message */ $message = sprintf( __( 'UPE payment failed: %s', 'woocommerce-payments' ), $e->getMessage() ); $this->order_service->mark_payment_failed( $order, $intent_id, $status, $charge_id, $message ); } wc_add_notice( WC_Payments_Utils::get_filtered_error_message( $e ), 'error' ); $redirect_url = wc_get_checkout_url(); if ( $is_order_id_mismatched_exception ) { $redirect_url = add_query_arg( self::PROCESS_REDIRECT_ORDER_MISMATCH_ERROR_CODE, 'yes', $redirect_url ); } wp_safe_redirect( $redirect_url ); exit; } } /** * Mandate must be shown and acknowledged by customer before deferred intent UPE payment can be processed. * This applies to SEPA and Link payment methods. * https://stripe.com/docs/payments/finalize-payments-on-the-server * * @return boolean True if mandate must be shown and acknowledged by customer before deferred intent UPE payment can be processed, false otherwise. */ public function is_mandate_data_required() { $is_stripe_link_enabled = Payment_Method::CARD === $this->get_selected_stripe_payment_type_id() && in_array( Payment_Method::LINK, $this->get_upe_enabled_payment_method_ids(), true ); $is_sepa_debit_payment = Payment_Method::SEPA === $this->get_selected_stripe_payment_type_id(); return $is_stripe_link_enabled || $is_sepa_debit_payment; } /** * Get the payment method chosen by the customer for the payment processing. * This payment method is needed in case of the deferred intent creation flow only, because this is the only time when the current gateway might process payments other than of the card type. * Payment method is only one in case of the deferred intent creation flow, hence the first element of the array is returned. * * @return string|null Payment method to use for the intent. */ public function get_payment_method_to_use_for_intent() { $requested_payment_method = sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification return $this->get_payment_methods_from_gateway_id( $requested_payment_method )[0]; } /** * Get payment method types to attach to intention request. * * @param Payment_Information $payment_information Payment information object for transaction. * @return array List of payment methods. */ public function get_payment_method_types( $payment_information ): array { // For Express Checkout payments, use the payment method types sent by the client. // These must match the types used to initialize Stripe Elements on the frontend. // phpcs:ignore WordPress.Security.NonceVerification if ( ! empty( $_POST['wcpay-express-payment-method-types'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $express_payment_method_types = json_decode( sanitize_text_field( wp_unslash( $_POST['wcpay-express-payment-method-types'] ) ), true ); if ( is_array( $express_payment_method_types ) && ! empty( $express_payment_method_types ) ) { return $express_payment_method_types; } } $requested_payment_method = sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification $token = $payment_information->get_payment_token(); if ( ! empty( $requested_payment_method ) ) { // All checkout requests should contain $_POST context, so we check this first. $payment_methods = $this->get_payment_methods_from_gateway_id( $requested_payment_method ); } elseif ( ! is_null( $token ) ) { // If $_POST is empty, this may be a subscription renewal, where saved payment token will be present instead. $order = $payment_information->get_order(); $order_id = $order instanceof WC_Order ? $order->get_id() : null; $payment_methods = $this->get_payment_methods_from_gateway_id( $token->get_gateway_id(), $order_id ); // For saved Link tokens (e.g., subscription renewals), ensure Link is included in payment method types // regardless of whether Link is currently enabled at checkout. The token was valid when saved // and should continue to work for renewals. if ( $token instanceof \WC_Payment_Token_WCPay_Link && ! in_array( Payment_Method::LINK, $payment_methods, true ) ) { $payment_methods[] = Payment_Method::LINK; } } return $payment_methods; } /** * Get the payment methods used in the request. * * @param string $gateway_id ID of processing payment gateway. * @param int $order_id ID of related order, if applicable. * @return array List of payment methods. */ public function get_payment_methods_from_gateway_id( $gateway_id, $order_id = null ) { $split_upe_gateway_prefix = self::GATEWAY_ID . '_'; // If $gateway_id begins with `woocommerce_payments_` payment method is a split UPE LPM. // Otherwise, $gateway_id must be `woocommerce_payments`. if ( substr( $gateway_id, 0, strlen( $split_upe_gateway_prefix ) ) === $split_upe_gateway_prefix ) { return [ str_replace( $split_upe_gateway_prefix, '', $gateway_id ) ]; } $eligible_payment_methods = WC_Payments::get_gateway()->get_payment_method_ids_enabled_at_checkout( $order_id, true ); // If $gateway_id is `woocommerce_payments`, this must be the CC gateway. // We only need to return single `card` payment method, adding `link` since Stripe Link is also supported. $payment_methods = [ Payment_Method::CARD ]; if ( in_array( Payment_Method::LINK, $eligible_payment_methods, true ) ) { $payment_methods[] = Payment_Method::LINK; } return $payment_methods; } /** * Get values for Stripe mandate_data parameter * * @return array mandate_data values to use in request. */ private function get_mandate_data() { return [ 'customer_acceptance' => [ 'type' => 'online', 'online' => [ 'ip_address' => WC_Geolocation::get_ip_address(), 'user_agent' => 'WooCommerce Payments/' . WCPAY_VERSION_NUMBER . '; ' . get_bloginfo( 'url' ), ], ], ]; } /** * Set formatted readable payment method title for order, * using payment method details from accompanying charge. * * @param \WC_Order $order WC Order being processed. * @param string $payment_method_type Stripe payment method key. * @param array|bool $payment_method_details Array of payment method details from charge or false. */ public function set_payment_method_title_for_order( $order, $payment_method_type, $payment_method_details ) { $payment_method = $this->get_selected_payment_method( $payment_method_type ); if ( ! $payment_method ) { return; } // express checkout can be Amazon Pay/Google Pay/Apple Pay/Link, // but Google Pay/Apple Pay/Link use the `card` gateway; Amazon Pay has its own gateway. $express_checkout_type = $order->get_meta( '_wcpay_express_checkout_payment_method' ); $effective_type = $express_checkout_type ? $express_checkout_type : $payment_method_type; $payment_methods_using_card = [ Payment_Method::CARD, Payment_Method::LINK, Payment_Method::GOOGLE_PAY, Payment_Method::APPLE_PAY ]; $payment_gateway = in_array( $effective_type, $payment_methods_using_card, true ) ? self::GATEWAY_ID : self::GATEWAY_ID . '_' . $effective_type; // this will ensure that the refunds are handled by the correct split gateway class. $order->set_payment_method( $payment_gateway ); // the Express Checkout handler already set the method's title in `tokenized_cart_set_payment_method_type`, earlier in the flow. if ( ! $express_checkout_type ) { $order->set_payment_method_title( $payment_method->get_title( $this->get_account_country(), $payment_method_details ) ); } $order->save(); } /** * Prepares Stripe metadata for a given order. The metadata later injected into intents, and * used in transactions listing/details. If merchant connects an account to new store, listing/details * keeps working even if orders are not available anymore - the metadata provides needed details. * * @param WC_Order $order Order being processed. * @param Payment_Type $payment_type Enum stating whether payment is single or recurring. * * @return array Array of keyed metadata values. */ protected function get_metadata_from_order( $order, $payment_type ) { $service = wcpay_get_container()->get( OrderService::class ); return $service->get_payment_metadata( $order->get_id(), $payment_type ); } /** * Given the charge data, checks if there was an exchange and adds it to the given order as metadata * * @param WC_Order $order The order to update. * @param string $charge_id ID of the charge to attach data from. */ public function attach_exchange_info_to_order( $order, $charge_id ) { if ( empty( $charge_id ) ) { return; } $currency_store = strtolower( get_option( 'woocommerce_currency' ) ); $currency_order = strtolower( $order->get_currency() ); $currency_account = strtolower( $this->account->get_account_default_currency() ); // If the default currency for the store is different from the currency for the merchant's Stripe account, // the conversion rate provided by Stripe won't make sense, so we should not attach it to the order meta data // and instead we'll rely on the _wcpay_multi_currency_order_exchange_rate meta key for analytics. if ( $currency_store !== $currency_account ) { return; } if ( $currency_order !== $currency_account ) { // We check that the currency used in the order is different than the one set in the WC Payments account // to avoid requesting the charge if not needed. $request = Get_Charge::create( $charge_id ); $request->set_hook_args( $charge_id ); $charge = $request->send(); $exchange_rate = $charge['balance_transaction']['exchange_rate'] ?? null; if ( isset( $exchange_rate ) ) { $exchange_rate = WC_Payments_Utils::interpret_string_exchange_rate( $exchange_rate, $currency_order, $currency_account ); $order->update_meta_data( '_wcpay_multi_currency_stripe_exchange_rate', $exchange_rate ); $order->save_meta_data(); } } } /** * Saves the payment token to the order. * * @param WC_Order $order The order. * @param WC_Payment_Token $token The token to save. */ public function add_token_to_order( $order, $token ) { $payment_token = $this->get_payment_token( $order ); // This could lead to tokens being saved twice in an order's payment tokens, but it is needed so that shoppers // may re-use a previous card for the same subscription, as we consider the last token to be the active one. // We can't remove the previous entry for the token because WC_Order does not support removal of tokens [1] and // we can't delete the token as it might be used somewhere else. // [1] https://github.com/woocommerce/woocommerce/issues/11857. if ( is_null( $payment_token ) || $token->get_id() !== $payment_token->get_id() ) { $order->add_payment_token( $token ); } $this->maybe_add_token_to_subscription_order( $order, $token ); } /** * Retrieve payment token from a subscription or order. * * @param WC_Order $order Order or subscription object. * * @return null|WC_Payment_Token Last token associated with order or subscription. */ protected function get_payment_token( $order ) { $order_tokens = $order->get_payment_tokens(); $token_id = end( $order_tokens ); return ! $token_id ? null : WC_Payment_Tokens::get( $token_id ); } /** * Can the order be refunded? * * @param WC_Order $order Order object. * @return bool */ public function can_refund_order( $order ) { return $order && $this->order_service->get_charge_id_for_order( $order ); } /** * Refund a charge. * * @param int $order_id - the Order ID to process the refund for. * @param float $amount - the amount to refund. * @param string $reason - the reason for refunding. * * @return bool|WP_Error - Whether the refund went through. Returns a WP_Error if an Exception occurs during execution. */ public function process_refund( $order_id, $amount = null, $reason = '' ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return false; } // If this order is not captured yet, don't try and refund it. Instead, return an appropriate error message. if ( Intent_Status::REQUIRES_CAPTURE === $this->order_service->get_intention_status_for_order( $order ) ) { return new WP_Error( 'uncaptured-payment', /* translators: an error message which will appear if a user tries to refund an order which is has been authorized but not yet charged. */ __( "This payment is not captured yet. To cancel this order, please go to 'Order Actions' > 'Cancel authorization'. To proceed with a refund, please go to 'Order Actions' > 'Capture charge' to charge the payment card, and then trigger a refund via the 'Refund' button.", 'woocommerce-payments' ) ); } // Refund without an amount is a no-op, but required to succeed in // case merchant needs it to re-stock order items. if ( '0.00' === sprintf( '%0.2f', $amount ?? 0 ) ) { return true; } // If the entered amount is not valid stop without making a request. if ( $amount < 0 || $amount > $order->get_total() ) { return new WP_Error( 'invalid-amount', __( 'The refund amount is not valid.', 'woocommerce-payments' ) ); } $charge_id = $this->order_service->get_charge_id_for_order( $order ); $currency = $this->order_service->get_wcpay_intent_currency_for_order( $order ); try { // If the payment method is Interac, the refund already exists (refunded via Mobile app). $is_refunded_off_session = Payment_Method::INTERAC_PRESENT === $this->get_payment_method_type_for_order( $order ); if ( $is_refunded_off_session ) { $refund_amount = WC_Payments_Utils::prepare_amount( $amount ?? $order->get_total(), $order->get_currency() ); $list_charge_refund_request = List_Charge_Refunds::create(); $list_charge_refund_request->set_charge( $charge_id ); $list_charge_refund_response = $list_charge_refund_request->send(); $refunds = array_filter( $list_charge_refund_response['data'] ?? [], static function ( $refund ) use ( $refund_amount ) { return 'succeeded' === $refund['status'] && $refund_amount === $refund['amount']; } ); if ( [] === $refunds ) { return new WP_Error( 'wcpay_edit_order_refund_not_possible', __( 'You shall refund this payment in the same application where the payment was made.', 'woocommerce-payments' ) ); } $refund = array_pop( $refunds ); } else { $refund_request = Refund_Charge::create(); $refund_request->set_charge( $charge_id ); if ( null !== $amount ) { $refund_request->set_amount( WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ) ); } // These are reasons supported by Stripe https://stripe.com/docs/api/refunds/create#create_refund-reason. if ( in_array( $reason, [ 'duplicate', 'fraudulent', 'requested_by_customer' ], true ) ) { $refund_request->set_reason( $reason ); } $refund = $refund_request->send(); } $currency = strtoupper( $refund['currency'] ); Tracker::track_admin( 'wcpay_edit_order_refund_success' ); } catch ( Exception $e ) { if ( $e instanceof API_Exception && 'insufficient_balance_for_refund' === $e->get_error_code() ) { // Handle insufficient_balance_for_refund error. $this->order_service->handle_insufficient_balance_for_refund( $order, WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ) ); } else { $note = sprintf( /* translators: %1: the successfully charged amount, %2: error message */ __( 'A refund of %1$s failed to complete: %2$s', 'woocommerce-payments' ), WC_Payments_Explicit_Price_Formatter::get_explicit_price( wc_price( $amount, [ 'currency' => $currency ] ), $order ), $e->getMessage() ); Logger::log( $note ); $order->add_order_note( $note ); } $this->order_service->set_wcpay_refund_status_for_order( $order, 'failed' ); Tracker::track_admin( 'wcpay_edit_order_refund_failure', [ 'reason' => $note ] ); return new WP_Error( 'wcpay_edit_order_refund_failure', $e->getMessage() ); } $wc_refund = WC_Payments_Utils::get_last_refund_from_order_id( $order->get_id() ); if ( ! $wc_refund ) { // translators: %1$: order id. return new WP_Error( 'wcpay_edit_order_refund_not_found', sprintf( __( 'A refund cannot be found for order: %1$s', 'woocommerce-payments' ), $order->get_id() ) ); } // There is no error. Refund status can be either pending or succeeded, add a note to the order and update the refund status. $this->order_service->add_note_and_metadata_for_created_refund( $order, $wc_refund, $refund['id'], $refund['balance_transaction'] ?? null, Refund_Status::PENDING === $refund['status'] ); return true; } /** * Checks whether a refund through the gateway has already failed. * * @param WC_Order $order The order to check. * @return boolean */ public function has_refund_failed( $order ) { return Refund_Status::FAILED === $this->order_service->get_wcpay_refund_status_for_order( $order ); } /** * Gets the payment method type used for an order, if any * * @param WC_Order $order The order to get the payment method type for. * * @return string */ private function get_payment_method_type_for_order( $order ): string { $payment_method_details = []; if ( $this->order_service->get_payment_method_id_for_order( $order ) ) { $payment_method_id = $this->order_service->get_payment_method_id_for_order( $order ); $payment_method_details = $this->payments_api_client->get_payment_method( $payment_method_id ); } elseif ( $this->order_service->get_intent_id_for_order( $order ) ) { $payment_intent_id = $this->order_service->get_intent_id_for_order( $order ); $request = Get_Intention::create( $payment_intent_id ); $request->set_hook_args( $order ); $payment_intent = $request->send(); $charge = $payment_intent ? $payment_intent->get_charge() : null; $payment_method_details = $charge ? $charge->get_payment_method_details() : []; } return $payment_method_details['type'] ?? 'unknown'; } /** * Get option from DB or connected account. * * Overrides parent method to retrieve some options from connected account. * * @param string $key Option key. * @param mixed $empty_value Value when empty. * @return string|array|int|bool The value specified for the option or a default value for the option. */ public function get_option( $key, $empty_value = null ) { switch ( $key ) { case 'enabled': return parent::get_option( static::METHOD_ENABLED_KEY, $empty_value ); case 'account_country': return $this->get_account_country(); case 'account_statement_descriptor': return $this->get_account_statement_descriptor(); case 'account_statement_descriptor_kanji': return $this->get_account_statement_descriptor_kanji(); case 'account_statement_descriptor_kana': return $this->get_account_statement_descriptor_kana(); case 'account_business_name': return $this->get_account_business_name(); case 'account_business_url': return $this->get_account_business_url(); case 'account_business_support_address': return $this->get_account_business_support_address(); case 'account_business_support_email': return $this->get_account_business_support_email(); case 'account_business_support_phone': return $this->get_account_business_support_phone(); case 'account_branding_logo': return $this->get_account_branding_logo(); case 'account_branding_icon': return $this->get_account_branding_icon(); case 'account_branding_primary_color': return $this->get_account_branding_primary_color(); case 'account_branding_secondary_color': return $this->get_account_branding_secondary_color(); case 'account_domestic_currency': return $this->get_account_domestic_currency(); case 'account_communications_email': return $this->get_account_communications_email(); case 'deposit_schedule_interval': return $this->get_deposit_schedule_interval(); case 'deposit_schedule_weekly_anchor': return $this->get_deposit_schedule_weekly_anchor(); case 'deposit_schedule_monthly_anchor': return $this->get_deposit_schedule_monthly_anchor(); case 'deposit_delay_days': return $this->get_deposit_delay_days(); case 'deposit_status': return $this->get_deposit_status(); case 'deposit_restrictions': return $this->get_deposit_restrictions(); case 'deposit_completed_waiting_period': return $this->get_deposit_completed_waiting_period(); case 'current_protection_level': return $this->get_current_protection_level(); case 'advanced_fraud_protection_settings': return $this->get_advanced_fraud_protection_settings(); default: return parent::get_option( $key, $empty_value ); } } /** * Return the name of the option in the WP DB. * Overrides parent method so the option key is the same as the parent class. */ public function get_option_key() { return $this->plugin_id . $this->id . '_settings'; } /** * Update a single option. * Overrides parent method to use different key for `enabled`. * * @param string $key Option key. * @param mixed $value Value to set. * @return bool was anything saved? */ public function update_option( $key, $value = '' ) { if ( 'enabled' === $key ) { $key = static::METHOD_ENABLED_KEY; } return parent::update_option( $key, $value ); } /** * Updates whether woopay is enabled or disabled. * * @param bool $is_woopay_enabled Whether woopay should be enabled. */ public function update_is_woopay_enabled( $is_woopay_enabled ) { $current_is_woopay_enabled = 'yes' === $this->get_option( 'platform_checkout', 'no' ); if ( $is_woopay_enabled !== $current_is_woopay_enabled ) { WC_Payments::woopay_tracker()->maybe_record_admin_event( $is_woopay_enabled ? 'woopay_enabled' : 'woopay_disabled', [ 'test_mode' => WC_Payments::mode()->is_test() ? 1 : 0 ] ); $this->update_option( 'platform_checkout', $is_woopay_enabled ? 'yes' : 'no' ); if ( ! $is_woopay_enabled ) { $this->update_option( 'platform_checkout_last_disable_date', gmdate( 'Y-m-d' ) ); } if ( ! $is_woopay_enabled ) { WooPay_Order_Status_Sync::remove_webhook(); } } } /** * Updates whether woopay global theme support is enabled or disabled. * * @param bool $value Whether woopay global theme support should be enabled. */ public function update_is_woopay_global_theme_support_enabled( $value ) { $current_value = 'yes' === $this->get_option( 'is_woopay_global_theme_support_enabled', 'no' ); if ( $value !== $current_value ) { WC_Payments::woopay_tracker()->maybe_record_admin_event( $value ? 'woopay_global_theme_support_enabled' : 'woopay_global_theme_support_disabled', [ 'test_mode' => WC_Payments::mode()->is_test() ? 1 : 0 ] ); $this->update_option( 'is_woopay_global_theme_support_enabled', $value ? 'yes' : 'no' ); } } /** * Checks whether the WooPay global theme support is enabled. * * @return bool The result. */ public function is_woopay_global_theme_support_enabled() { return WC_Payments_Features::is_woopay_global_theme_support_eligible() && 'yes' === $this->get_option( 'is_woopay_global_theme_support_enabled' ); } /** * Init settings for gateways. */ public function init_settings() { parent::init_settings(); $this->enabled = ! empty( $this->settings[ static::METHOD_ENABLED_KEY ] ) && 'yes' === $this->settings[ static::METHOD_ENABLED_KEY ] ? 'yes' : 'no'; } /** * Get payment capture type from WCPay settings. * * @return Payment_Capture_Type MANUAL or AUTOMATIC depending on the settings. */ protected function get_capture_type() { return 'yes' === $this->get_option( 'manual_capture' ) ? Payment_Capture_Type::MANUAL() : Payment_Capture_Type::AUTOMATIC(); } /** * Map fields that need to be updated and update the fields server side. * * @param array $settings Plugin settings. * * @return array|WP_Error Updated fields. */ public function update_account_settings( array $settings ) { $account_settings = []; foreach ( static::ACCOUNT_SETTINGS_MAPPING as $name => $account_key ) { if ( isset( $settings[ $name ] ) ) { $account_settings[ $account_key ] = $settings[ $name ]; } } $result = $this->update_account( $account_settings ); if ( is_wp_error( $result ) ) { return $result; } return $account_settings; } /** * Gets connected account statement descriptor. * * @param string $empty_value Empty value to return when not connected or fails to fetch account descriptor. * * @return string Statement descriptor of default value. */ public function get_account_statement_descriptor( string $empty_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_statement_descriptor(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account statement descriptor.' . $e ); } return $empty_value; } /** * Gets connected account statement descriptor. * * @param string $empty_value Empty value to return when not connected or fails to fetch account descriptor. * * @return string Statement descriptor of default value. */ public function get_account_statement_descriptor_kanji( string $empty_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_statement_descriptor_kanji(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account statement descriptor.' . $e ); } return $empty_value; } /** * Gets connected account statement descriptor. * * @param string $empty_value Empty value to return when not connected or fails to fetch account descriptor. * * @return string Statement descriptor of default value. */ public function get_account_statement_descriptor_kana( string $empty_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_statement_descriptor_kana(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account statement descriptor.' . $e ); } return $empty_value; } /** * Gets connected account business name. * * @param string $default_value Value to return when not connected or failed to fetch business name. * * @return string Business name or default value. */ protected function get_account_business_name( $default_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_business_name(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account business name.' . $e ); } return $default_value; } /** * Gets connected account business url. * * @param string $default_value Value to return when not connected or failed to fetch business url. * * @return string Business url or default value. */ protected function get_account_business_url( $default_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_business_url(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account business URL.' . $e ); } return $default_value; } /** * Gets connected account business address. * * @param array $default_value Value to return when not connected or failed to fetch business address. * * @return array Business address or default value. */ protected function get_account_business_support_address( $default_value = [] ): array { try { if ( $this->is_connected() ) { return $this->account->get_business_support_address(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account business support address.' . $e ); } return $default_value; } /** * Gets connected account business support email. * * @param string $default_value Value to return when not connected or failed to fetch business support email. * * @return string Business support email or default value. */ protected function get_account_business_support_email( $default_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_business_support_email(); } } catch ( Exception $e ) { Logger::error( 'Failed to get business support email.' . $e ); } return $default_value; } /** * Gets connected account business support phone. * * @param string $default_value Value to return when not connected or failed to fetch business support phone. * * @return string Business support phone or default value. */ protected function get_account_business_support_phone( $default_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_business_support_phone(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account business support phone.' . $e ); } return $default_value; } /** * Gets connected account branding logo. * * @param string $default_value Value to return when not connected or failed to fetch branding logo. * * @return string Business support branding logo or default value. */ protected function get_account_branding_logo( $default_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_branding_logo(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account branding logo.' . $e ); } return $default_value; } /** * Gets connected account branding icon. * * @param string $default_value Value to return when not connected or failed to fetch branding icon. * * @return string Business support branding icon or default value. */ protected function get_account_branding_icon( $default_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_branding_icon(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account\'s branding icon.' . $e ); } return $default_value; } /** * Gets connected account branding primary color. * * @param string $default_value Value to return when not connected or failed to fetch branding primary color. * * @return string Business support branding primary color or default value. */ protected function get_account_branding_primary_color( $default_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_branding_primary_color(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account\'s branding primary color.' . $e ); } return $default_value; } /** * Gets connected account branding secondary color. * * @param string $default_value Value to return when not connected or failed to fetch branding secondary color. * * @return string Business support branding secondary color or default value. */ protected function get_account_branding_secondary_color( $default_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_branding_secondary_color(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account\'s branding secondary color.' . $e ); } return $default_value; } /** * Gets connected account communications email. * * @param string $default_value Value to return when not connected or failed to fetch communications email. * * @return string Communications email or default value. */ protected function get_account_communications_email( $default_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_communications_email(); } } catch ( Exception $e ) { Logger::error( 'Failed to get account\'s communication email.' . $e ); } return $default_value; } /** * Retrieves the domestic currency of the current account based on its country. * It will fallback to the store's currency if the account's country is not supported. * * @return string The domestic currency code. */ public function get_account_domestic_currency(): string { $merchant_country = strtoupper( $this->account->get_account_country() ); $country_locale_data = $this->localization_service->get_country_locale_data( $merchant_country ); // Check for missing country locale data. if ( ! isset( $country_locale_data['currency_code'] ) ) { Logger::error( sprintf( 'Could not find locale data for merchant country: %s. Falling back to the merchant\'s default currency.', $merchant_country ) ); return $this->account->get_account_default_currency(); } return $country_locale_data['currency_code']; } /** * Gets connected account deposit schedule interval. * * @param string $empty_value Empty value to return when not connected or fails to fetch deposit schedule. * * @return string Interval or default value. */ protected function get_deposit_schedule_interval( string $empty_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_deposit_schedule_interval(); } } catch ( Exception $e ) { Logger::error( 'Failed to get deposit schedule interval.' . $e ); } return $empty_value; } /** * Gets connected account deposit schedule weekly anchor. * * @param string $empty_value Empty value to return when not connected or fails to fetch deposit schedule weekly anchor. * * @return string Weekly anchor or default value. */ protected function get_deposit_schedule_weekly_anchor( string $empty_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_deposit_schedule_weekly_anchor(); } } catch ( Exception $e ) { Logger::error( 'Failed to get deposit schedule weekly anchor.' . $e ); } return $empty_value; } /** * Gets connected account deposit schedule monthly anchor. * * @param int|null $empty_value Empty value to return when not connected or fails to fetch deposit schedule monthly anchor. * * @return int|null Monthly anchor or default value. */ protected function get_deposit_schedule_monthly_anchor( $empty_value = null ) { try { if ( $this->is_connected() ) { return $this->account->get_deposit_schedule_monthly_anchor(); } } catch ( Exception $e ) { Logger::error( 'Failed to get deposit schedule monthly anchor.' . $e ); } return null === $empty_value ? null : (int) $empty_value; } /** * Gets connected account deposit delay days. * * @param int $default_value Value to return when not connected or fails to fetch deposit delay days. Default is 7 days. * * @return int number of days. */ protected function get_deposit_delay_days( int $default_value = 7 ): int { try { if ( $this->is_connected() ) { return $this->account->get_deposit_delay_days() ?? $default_value; } } catch ( Exception $e ) { Logger::error( 'Failed to get deposit delay days.' . $e ); } return $default_value; } /** * Gets connected account country. * * @param string $default_value Value to return when not connected or fails to fetch account details. Default is US. * * @return string code of the country. */ public function get_account_country( string $default_value = Country_Code::UNITED_STATES ): string { try { if ( $this->is_connected() ) { return $this->account->get_account_country() ?? $default_value; } } catch ( Exception $e ) { Logger::error( 'Failed to get account country.' . $e ); } return $default_value; } /** * Gets connected account deposit status. * * @param string $empty_value Empty value to return when not connected or fails to fetch deposit status. * * @return string deposit status or default value. */ protected function get_deposit_status( string $empty_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_deposit_status(); } } catch ( Exception $e ) { Logger::error( 'Failed to get deposit status.' . $e ); } return $empty_value; } /** * Gets connected account deposit restrictions. * * @param string $empty_value Empty value to return when not connected or fails to fetch deposit restrictions. * * @return string deposit restrictions or default value. */ protected function get_deposit_restrictions( string $empty_value = '' ): string { try { if ( $this->is_connected() ) { return $this->account->get_deposit_restrictions(); } } catch ( Exception $e ) { Logger::error( 'Failed to get deposit restrictions.' . $e ); } return $empty_value; } /** * Gets the completed deposit waiting period value. * * @param bool $empty_value Empty value to return when not connected or fails to fetch the completed deposit waiting period value. * * @return bool The completed deposit waiting period value or default value. */ protected function get_deposit_completed_waiting_period( bool $empty_value = false ): bool { try { if ( $this->is_connected() ) { return $this->account->get_deposit_completed_waiting_period(); } } catch ( Exception $e ) { Logger::error( 'Failed to get the deposit waiting period value.' . $e ); } return $empty_value; } /** * Gets the current fraud protection level value. * * @return string The current fraud protection level. */ protected function get_current_protection_level() { $this->maybe_refresh_fraud_protection_settings(); return get_option( 'current_protection_level', 'basic' ); } /** * Gets the advanced fraud protection level settings value. * * @return array|string The advanced level fraud settings for the store, if not saved, the default ones. * If there's a fetch error, it returns "error". */ protected function get_advanced_fraud_protection_settings() { // Check if Stripe is connected. if ( ! $this->is_connected() ) { return []; } $this->maybe_refresh_fraud_protection_settings(); $transient_value = get_transient( 'wcpay_fraud_protection_settings' ); return false === $transient_value ? 'error' : $transient_value; } /** * Checks if a fraud protection rule is enabled. * * @param string $rule The rule to check. * * @return bool True if the rule is enabled, false otherwise. */ protected function is_fraud_rule_enabled( string $rule ): bool { $settings = $this->get_advanced_fraud_protection_settings(); if ( ! is_array( $settings ) ) { return false; } foreach ( $settings as $setting ) { if ( $rule === $setting['key'] ) { return true; } } return false; } /** * Checks if the transaction was blocked by AVS verification fraud rule. * * @param string|null $error_code The error code to check. * @param string|null $error_type The error type to check. * * @return bool True if the transaction was blocked by the AVS verification fraud rule, false otherwise. */ private function is_blocked_by_avs_verification_fraud_rule( ?string $error_code, ?string $error_type ): bool { $is_avs_verification_rule_enabled = $this->is_fraud_rule_enabled( 'avs_verification' ); $is_incorrect_zip_error = 'card_error' === $error_type && 'incorrect_zip' === $error_code; return $is_avs_verification_rule_enabled && $is_incorrect_zip_error; } /** * Checks if the transaction was blocked by fraud rules. * * @param Exception $e The exception to check. * * @return bool True if the transaction was blocked by fraud rules, false otherwise. */ protected function is_blocked_by_fraud_rules( Exception $e ): bool { if ( ! ( $e instanceof API_Exception ) ) { return false; } $error_code = $e->get_error_code() ?? null; $error_type = $e->get_error_type() ?? null; $blocked_by_fraud_rule = 'wcpay_blocked_by_fraud_rule' === $error_code; // Since the AVS mismatch is part of the advanced fraud prevention, we need to consider that as a blocked order. $blocked_by_avs_mismatch = $this->is_blocked_by_avs_verification_fraud_rule( $error_code, $error_type ); return $blocked_by_fraud_rule || $blocked_by_avs_mismatch; } /** * Checks the synchronicity of fraud protection settings with the server, and updates the local cache when needed. * * @return void */ protected function maybe_refresh_fraud_protection_settings() { // It'll be good to run this only once per call, because if it succeeds, the latter won't require // to run again, and if it fails, it will fail on other calls too. static $runonce = false; // If already ran this before on this call, return. if ( $runonce ) { return; } // Check if we have local cache available before pulling it from the server. // If the transient exists, do nothing. $cached_server_settings = get_transient( 'wcpay_fraud_protection_settings' ); if ( false === $cached_server_settings ) { // When both local and server values don't exist, we need to reset the protection level on both to "Basic". $needs_reset = false; try { // There's no cached ruleset, or the cache has expired. Try to fetch it from the server. $latest_server_ruleset = $this->payments_api_client->get_latest_fraud_ruleset(); if ( isset( $latest_server_ruleset['ruleset_config'] ) ) { // Update the local cache from the server. set_transient( 'wcpay_fraud_protection_settings', $latest_server_ruleset['ruleset_config'], DAY_IN_SECONDS ); // Get the matching level for the ruleset, and set the option. update_option( 'current_protection_level', Fraud_Risk_Tools::get_matching_protection_level( $latest_server_ruleset['ruleset_config'] ) ); return; } // If the response doesn't contain a ruleset, probably there's an error. Grey out the form. } catch ( API_Exception $ex ) { if ( 'wcpay_fraud_ruleset_not_found' === $ex->get_error_code() ) { // If fetching returned a 'wcpay_fraud_ruleset_not_found' exception, save the basic protection as the server ruleset, // and update the client with the same config. $needs_reset = true; } // If the exception isn't what we want, probably there's an error. Grey out the form. } if ( $needs_reset ) { // Set the Basic protection level as the default on both client and server. $basic_protection_settings = Fraud_Risk_Tools::get_basic_protection_settings(); $this->payments_api_client->save_fraud_ruleset( $basic_protection_settings ); set_transient( 'wcpay_fraud_protection_settings', $basic_protection_settings, DAY_IN_SECONDS ); update_option( 'current_protection_level', 'basic' ); } // Set the static flag to prevent duplicate calls to this method. $runonce = true; } } /** * Updates the fraud rules depending on some settings when those settings have changed. * * @return void This is a readonly action. */ public function update_fraud_rules_based_on_general_options() { // If the protection level is not "advanced", no need to run this, because it won't contain the IP country filter. if ( 'advanced' !== $this->get_current_protection_level() ) { return; } // If the ruleset can't be parsed, skip updating. $ruleset = $this->get_advanced_fraud_protection_settings(); if ( 'error' === $ruleset || ! is_array( $ruleset ) || ! Fraud_Risk_Tools::is_valid_ruleset_array( $ruleset ) ) { return; } $needs_update = false; foreach ( $ruleset as &$rule_array ) { if ( isset( $rule_array['key'] ) && Fraud_Risk_Tools::RULE_INTERNATIONAL_IP_ADDRESS === $rule_array['key'] ) { $new_rule_array = Fraud_Risk_Tools::get_international_ip_address_rule()->to_array(); if ( isset( $rule_array['check'] ) && isset( $new_rule_array['check'] ) && wp_json_encode( $rule_array['check'] ) !== wp_json_encode( $new_rule_array['check'] ) ) { $rule_array = $new_rule_array; $needs_update = true; } } } // Update the possibly changed values on the server, and the transient. if ( $needs_update ) { $this->payments_api_client->save_fraud_ruleset( $ruleset ); set_transient( 'wcpay_fraud_protection_settings', $ruleset, DAY_IN_SECONDS ); } } /** * The URL for the current payment method's icon. * * @return string The payment method icon URL. */ public function get_icon_url() { return $this->payment_method->get_icon(); } /** * Handles connected account update when plugin settings saved. * * Adds error message to display in admin notices in case of failure. * * @param array $account_settings Stripe account settings. * Supported: statement_descriptor, business_name, business_url, business_support_address, * business_support_email, business_support_phone, branding_logo, branding_icon, * branding_primary_color, branding_secondary_color. * * $return array | WP_Error Update account result. * * @throws Exception */ public function update_account( $account_settings ) { if ( empty( $account_settings ) ) { return; } $stripe_account_update_response = $this->account->update_stripe_account( $account_settings ); if ( is_wp_error( $stripe_account_update_response ) ) { $msg = __( 'Failed to update Stripe account. ', 'woocommerce-payments' ) . $stripe_account_update_response->get_error_message(); $this->add_error( $msg ); } return $stripe_account_update_response; } /** * Validates statement descriptor value * * @param string $key Field key. * @param string $value Posted Value. * * @return string Sanitized statement descriptor. * @throws InvalidArgumentException When statement descriptor is invalid. */ public function validate_account_statement_descriptor_field( $key, $value ) { // Since the value is escaped, and we are saving in a place that does not require escaping, apply stripslashes. $value = trim( stripslashes( $value ) ); // Validation can be done with a single regex but splitting into multiple for better readability. $valid_length = '/^.{5,22}$/'; $has_one_letter = '/^.*[a-zA-Z]+/'; $no_specials = '/^[^*"\'<>]*$/'; if ( ! preg_match( $valid_length, $value ) || ! preg_match( $has_one_letter, $value ) || ! preg_match( $no_specials, $value ) ) { throw new InvalidArgumentException( __( 'Customer bank statement is invalid. Statement should be between 5 and 22 characters long, contain at least single Latin character and does not contain special characters: \' " * < >', 'woocommerce-payments' ) ); } return $value; } /** * Add capture and cancel actions for orders with an authorized charge. * * @param array $actions - Actions to make available in order actions metabox. */ public function add_order_actions( $actions ) { global $theorder; if ( ! is_object( $theorder ) ) { return $actions; } if ( $this->id !== $theorder->get_payment_method() ) { return $actions; } if ( Intent_Status::REQUIRES_CAPTURE !== $this->order_service->get_intention_status_for_order( $theorder ) ) { return $actions; } // if order is already completed, we shouldn't capture the charge anymore. if ( in_array( $theorder->get_status(), wc_get_is_paid_statuses(), true ) ) { return $actions; } $new_actions = [ 'capture_charge' => __( 'Capture charge', 'woocommerce-payments' ), 'cancel_authorization' => __( 'Cancel authorization', 'woocommerce-payments' ), ]; return array_merge( $new_actions, $actions ); } /** * Capture previously authorized charge. * * @param WC_Order $order - Order to capture charge on. * @param bool $include_level3 - Whether to include level 3 data in payment intent. * @param array $intent_metadata - Intent metadata retrieved earlier in the calling method. * * @return array An array containing the status (succeeded/failed), id (intent ID), message (error message if any), and http code */ public function capture_charge( $order, $include_level3 = true, $intent_metadata = [] ) { $intent_id = null; $amount = $order->get_total(); $is_authorization_expired = false; $intent = null; $status = null; $error_message = null; $http_code = null; $error_code = null; try { $intent_id = $order->get_transaction_id(); $payment_type = $this->is_payment_recurring( $order->get_id() ) ? Payment_Type::RECURRING() : Payment_Type::SINGLE(); $metadata_from_order = $this->get_metadata_from_order( $order, $payment_type ); $merged_metadata = array_merge( (array) $metadata_from_order, (array) $intent_metadata ); // prioritize metadata from mobile app. $capture_intention_request = Capture_Intention::create( $intent_id ); $capture_intention_request->set_amount_to_capture( WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ) ); $capture_intention_request->set_metadata( $merged_metadata ); $capture_intention_request->set_hook_args( $order ); if ( $include_level3 ) { $capture_intention_request->set_level3( $this->get_level3_data_from_order( $order ) ); } $intent = $capture_intention_request->send(); $status = $intent->get_status(); $http_code = 200; } catch ( API_Exception $e ) { try { $error_message = $e->getMessage(); $http_code = $e->get_http_code(); $error_code = $e->get_error_code(); $extra_details = []; if ( $e instanceof Amount_Too_Small_Exception ) { $extra_details = [ 'minimum_amount' => $e->get_minimum_amount(), 'minimum_amount_currency' => strtoupper( $e->get_currency() ), ]; $minimum_amount_details = sprintf( /* translators: %s: formatted minimum amount with currency */ __( 'The minimum amount to capture is %s.', 'woocommerce-payments' ), WC_Payments_Utils::format_explicit_currency( WC_Payments_Utils::interpret_stripe_amount( $e->get_minimum_amount(), $e->get_currency() ), $e->get_currency() ) ); $error_message = $error_message . ' ' . $minimum_amount_details; } $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); // Fetch the Intent to check if it's already expired and the site missed the "charge.expired" webhook. $intent = $request->send(); if ( Intent_Status::CANCELED === $intent->get_status() ) { $is_authorization_expired = true; } } catch ( API_Exception $ge ) { // Ignore any errors during the intent retrieval, and add the failed capture note below with the // original error message. $status = null; $error_message = $e->getMessage(); $http_code = $e->get_http_code(); $error_code = $e->get_error_code(); } } Tracker::track_admin( 'wcpay_merchant_captured_auth' ); // There is a possibility of the intent being null, so we need to get the charge_id safely. $charge = ! empty( $intent ) ? $intent->get_charge() : null; $charge_id = ! empty( $charge ) ? $charge->get_id() : $this->order_service->get_charge_id_for_order( $order ); $this->attach_exchange_info_to_order( $order, $charge_id ); if ( Intent_Status::SUCCEEDED === $status ) { $this->order_service->process_captured_payment( $order, $intent ); } elseif ( $is_authorization_expired ) { $this->order_service->mark_payment_capture_expired( $order, $intent_id, Intent_Status::CANCELED, $charge_id ); } else { if ( ! empty( $error_message ) ) { $error_message = esc_html( $error_message ); } else { $http_code = 502; } $this->order_service->mark_payment_capture_failed( $order, $intent_id, Intent_Status::REQUIRES_CAPTURE, $charge_id, $error_message ); } return [ 'status' => $status ?? 'failed', 'id' => ! empty( $intent ) ? $intent->get_id() : null, 'message' => $error_message, 'http_code' => $http_code, 'error_code' => $error_code, 'extra_details' => $extra_details ?? [], ]; } /** * Cancel previously authorized charge. * * @param WC_Order $order - Order to cancel authorization on. */ public function cancel_authorization( $order ) { $intent = null; $status = null; $error_message = null; $http_code = null; try { $request = Cancel_Intention::create( $order->get_transaction_id() ); $request->set_hook_args( $order ); $intent = $request->send(); $status = $intent->get_status(); $http_code = 200; } catch ( API_Exception $e ) { try { // Fetch the Intent to check if it's already expired and the site missed the "charge.expired" webhook. $request = Get_Intention::create( $order->get_transaction_id() ); $request->set_hook_args( $order ); $intent = $request->send(); $status = $intent->get_status(); if ( Intent_Status::CANCELED !== $status ) { $error_message = $e->getMessage(); } } catch ( API_Exception $ge ) { // Ignore any errors during the intent retrieval, and add the failed cancellation note below with the // original error message. $status = null; $error_message = $e->getMessage(); $http_code = $e->get_http_code(); } } if ( Intent_Status::CANCELED === $status ) { $this->order_service->update_order_status_from_intent( $order, $intent ); } else { if ( ! empty( $error_message ) ) { $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: error message */ __( 'Canceling authorization <strong>failed</strong> to complete with the following message: <code>%1$s</code>.', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'code' => '<code>', ] ), esc_html( $error_message ) ); $order->add_order_note( $note ); } else { $order->add_order_note( WC_Payments_Utils::esc_interpolated_html( __( 'Canceling authorization <strong>failed</strong> to complete.', 'woocommerce-payments' ), [ 'strong' => '<strong>' ] ) ); } $this->order_service->set_intention_status_for_order( $order, $status ); $order->save(); $http_code = 502; } return [ 'status' => $status ?? 'failed', 'id' => ! empty( $intent ) ? $intent->get_id() : null, 'message' => $error_message, 'http_code' => $http_code, ]; } /** * Create the level 3 data array to send to Stripe when making a purchase. * * @param WC_Order $order The order that is being paid for. * @return array The level 3 data to send to Stripe. */ public function get_level3_data_from_order( WC_Order $order ): array { return wcpay_get_container()->get( Level3Service::class )->get_data_from_order( $order->get_id() ); } /** * Handle AJAX request after authenticating payment at checkout. * * This function is used to update the order status after the user has * been asked to authenticate their payment. * * This function is used for both: * - regular checkout * - Pay for Order page * * @throws Exception - If nonce is invalid. */ public function update_order_status() { $intent_id_received = null; $order = null; try { $is_nonce_valid = check_ajax_referer( 'wcpay_update_order_status_nonce', false, false ); if ( ! $is_nonce_valid ) { throw new Process_Payment_Exception( __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-payments' ), 'invalid_referrer' ); } $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : false; $order = wc_get_order( $order_id ); if ( ! $order ) { throw new Process_Payment_Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-payments' ), 'order_not_found' ); } $intent_id = $this->order_service->get_intent_id_for_order( $order ); $intent_id_received = isset( $_POST['intent_id'] ) ? sanitize_text_field( wp_unslash( $_POST['intent_id'] ) ) /* translators: This will be used to indicate an unknown value for an ID. */ : __( 'unknown', 'woocommerce-payments' ); if ( empty( $intent_id ) ) { throw new Intent_Authentication_Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-payments' ), 'empty_intent_id' ); } // Check that the intent saved in the order matches the intent used as part of the // authentication process. The ID of the intent used is sent with // the AJAX request. We are about to use the status of the intent saved in // the order, so we need to make sure the intent that was used for authentication // is the same as the one we're using to update the status. if ( $intent_id !== $intent_id_received ) { throw new Intent_Authentication_Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-payments' ), 'intent_id_mismatch' ); } $amount = $order->get_total(); $payment_method_details = false; $is_changing_payment = isset( $_POST['is_changing_payment'] ) && filter_var( wp_unslash( $_POST['is_changing_payment'] ), FILTER_VALIDATE_BOOLEAN ); if ( $amount > 0 && ! $is_changing_payment ) { // An exception is thrown if an intent can't be found for the given intent ID. $request = Get_Intention::create( $intent_id ); $request->set_hook_args( $order ); $intent = $request->send(); $status = $intent->get_status(); $charge = $intent->get_charge(); $charge_id = ! empty( $charge ) ? $charge->get_id() : null; $this->attach_exchange_info_to_order( $order, $charge_id ); $this->order_service->attach_intent_info_to_order( $order, $intent ); $this->order_service->attach_transaction_fee_to_order( $order, $charge ); } else { // For $0 orders, fetch the Setup Intent instead. $setup_intent_request = Get_Setup_Intention::create( $intent_id ); /** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort $intent = $setup_intent_request->send(); $status = $intent->get_status(); $charge_id = ''; } $payment_method_id = $intent->get_payment_method_id(); if ( Intent_Status::SUCCEEDED === $status ) { $this->duplicate_payment_prevention_service->remove_session_processing_order( $order->get_id() ); } $this->order_service->update_order_status_from_intent( $order, $intent ); if ( $intent->is_authorized() ) { wc_maybe_reduce_stock_levels( $order_id ); WC()->cart->empty_cart(); $is_subscription = function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order ); $should_save_payment_method = $is_subscription || ( isset( $_POST['should_save_payment_method'] ) && 'true' === $_POST['should_save_payment_method'] ); if ( $should_save_payment_method && ! empty( $payment_method_id ) ) { try { $token = $this->token_service->add_payment_method_to_user( $payment_method_id, wp_get_current_user() ); $this->add_token_to_order( $order, $token ); if ( ! empty( $token ) ) { $payment_method_type = $this->get_payment_method_type_for_setup_intent( $intent, $token ); $this->set_payment_method_title_for_order( $order, $payment_method_type, $payment_method_details ); } } catch ( Exception $e ) { // If saving the token fails, log the error message but catch the error to avoid crashing the checkout flow. Logger::log( 'Error when saving payment method: ' . $e->getMessage() ); } } $return_url = $this->get_return_url( $order ); if ( $is_changing_payment ) { $payment_token = $this->get_payment_token( $order ); if ( class_exists( 'WC_Subscriptions_Change_Payment_Gateway' ) ) { WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $order, $payment_token->get_gateway_id() ); $notice = __( 'Payment method updated.', 'woocommerce-payments' ); if ( WC_Subscriptions_Change_Payment_Gateway::will_subscription_update_all_payment_methods( $order ) && WC_Subscriptions_Change_Payment_Gateway::update_all_payment_methods_from_subscription( $order, $token->get_gateway_id() ) ) { $notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-payments' ); } wc_add_notice( $notice ); } $return_url = method_exists( $order, 'get_view_order_url' ) ? $order->get_view_order_url() : $this->get_return_url( $order ); } // Send back redirect URL in the successful case. echo wp_json_encode( [ 'return_url' => $return_url, ] ); wp_die(); } } catch ( Intent_Authentication_Exception $e ) { $error_code = $e->get_error_code(); switch ( $error_code ) { case 'intent_id_mismatch': case 'empty_intent_id': // The empty_intent_id case needs the same handling. $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: transaction ID of the payment or a translated string indicating an unknown ID. */ __( 'A payment with ID <code>%1$s</code> was used in an attempt to pay for this order. This payment intent ID does not match any payments for this order, so it was ignored and the order was not updated.', 'woocommerce-payments' ), [ 'code' => '<code>', ] ), $intent_id_received ); $order->add_order_note( $note ); break; } // Send back error so it can be displayed to the customer. echo wp_json_encode( [ 'error' => [ 'message' => $e->getMessage(), ], ] ); wp_die(); } catch ( Exception $e ) { // Send back error so it can be displayed to the customer. echo wp_json_encode( [ 'error' => [ 'message' => $e->getMessage(), ], ] ); wp_die(); } } /** * Add payment method via account screen. * * @throws Add_Payment_Method_Exception If payment method is missing. */ public function add_payment_method() { try { // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! isset( $_POST['wcpay-setup-intent'] ) ) { throw new Add_Payment_Method_Exception( sprintf( /* translators: %s: WooPayments */ __( 'A %s payment method was not provided', 'woocommerce-payments' ), 'WooPayments' ), 'payment_method_intent_not_provided' ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash $setup_intent_id = ! empty( $_POST['wcpay-setup-intent'] ) ? wc_clean( $_POST['wcpay-setup-intent'] ) : false; $customer_id = $this->customer_service->get_customer_id_by_user_id( get_current_user_id() ); if ( ! $setup_intent_id || null === $customer_id ) { throw new Add_Payment_Method_Exception( __( "We're not able to add this payment method. Please try again later", 'woocommerce-payments' ), 'invalid_setup_intent_id' ); } $setup_intent_request = Get_Setup_Intention::create( $setup_intent_id ); /** @var WC_Payments_API_Setup_Intention $setup_intent */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort $setup_intent = $setup_intent_request->send(); if ( Intent_Status::SUCCEEDED !== $setup_intent->get_status() ) { throw new Add_Payment_Method_Exception( __( 'Failed to add the provided payment method. Please try again later', 'woocommerce-payments' ), 'invalid_response_status' ); } $payment_method = $setup_intent->get_payment_method_id(); $this->token_service->add_payment_method_to_user( $payment_method, wp_get_current_user() ); return [ 'result' => 'success', 'redirect' => apply_filters( 'wcpay_get_add_payment_method_redirect_url', wc_get_endpoint_url( 'payment-methods' ) ), ]; } catch ( Exception $e ) { wc_add_notice( WC_Payments_Utils::get_filtered_error_message( $e ), 'error', [ 'icon' => 'error' ] ); Logger::log( 'Error when adding payment method: ' . $e->getMessage() ); return [ 'result' => 'error', ]; } } /** * When an order is created/updated, we want to add an ActionScheduler job to send this data to * the payment server. * * @param int $order_id The ID of the order that has been created. * @param WC_Order|null $order The order that has been created. */ public function schedule_order_tracking( $order_id, $order = null ) { $this->maybe_schedule_subscription_order_tracking( $order_id, $order ); // If Sift is not enabled, exit out and don't do the tracking here. if ( ! isset( $this->fraud_service->get_fraud_services_config()['sift'] ) ) { return; } // Sometimes the woocommerce_update_order hook might be called with just the order ID parameter, // so we need to fetch the order here. if ( is_null( $order ) ) { $order = wc_get_order( $order_id ); } // We only want to track orders created by our payment gateway, and orders with a payment method set. if ( $order->get_payment_method() !== self::GATEWAY_ID || empty( $this->order_service->get_payment_method_id_for_order( $order ) ) ) { return; } // Check whether this is an order we haven't previously tracked a creation event for. if ( $order->get_meta( '_new_order_tracking_complete' ) !== 'yes' ) { // Schedule the action to send this information to the payment server. $this->action_scheduler_service->schedule_job( strtotime( '+5 seconds' ), 'wcpay_track_new_order', [ 'order_id' => $order_id ] ); } else { // Schedule an update action to send this information to the payment server. $this->action_scheduler_service->schedule_job( strtotime( '+5 seconds' ), 'wcpay_track_update_order', [ 'order_id' => $order_id ] ); } } /** * Create a setup intent when adding cards using the my account page. * * @return WC_Payments_API_Setup_Intention * * @throws API_Exception * @throws \WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception * @throws \WCPay\Core\Exceptions\Server\Request\Immutable_Parameter_Exception * @throws \WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception */ public function create_and_confirm_setup_intent() { $payment_information = Payment_Information::from_payment_request( $_POST, null, null, null, null, $this->get_payment_method_to_use_for_intent() ); // phpcs:ignore WordPress.Security.NonceVerification $should_save_in_platform_account = false; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['save_payment_method_in_platform_account'] ) && filter_var( wp_unslash( $_POST['save_payment_method_in_platform_account'] ), FILTER_VALIDATE_BOOLEAN ) ) { $should_save_in_platform_account = true; } // Determine the customer adding the payment method, create one if we don't have one already. $user = wp_get_current_user(); $customer_id = $this->customer_service->get_customer_id_by_user_id( $user->ID ); if ( null === $customer_id ) { $customer_data = WC_Payments_Customer_Service::map_customer_data( null, new WC_Customer( $user->ID ) ); $customer_id = $this->customer_service->create_customer_for_user( $user, $customer_data ); } $request = Create_And_Confirm_Setup_Intention::create(); $request->set_customer( $customer_id ); $request->set_payment_method( $payment_information->get_payment_method() ); $request->assign_hook( 'wcpay_create_and_confirm_setup_intention_request' ); $request->set_hook_args( $payment_information, $should_save_in_platform_account, false ); return $request->send(); } /** * Handle AJAX request for creating a setup intent when adding cards using the my account page. * * @throws Add_Payment_Method_Exception - If nonce or setup intent is invalid. */ public function create_setup_intent_ajax() { try { if ( ! check_ajax_referer( 'wcpay_create_setup_intent_nonce', false, false ) ) { throw new Add_Payment_Method_Exception( __( "We're not able to add this payment method. Please refresh the page and try again.", 'woocommerce-payments' ), 'invalid_referrer' ); } if ( WC_Rate_Limiter::retried_too_soon( 'add_payment_method_' . get_current_user_id() ) ) { throw new Add_Payment_Method_Exception( __( 'You cannot add a new payment method so soon after the previous one. Please try again later.', 'woocommerce-payments' ), 'retried_too_soon' ); } $setup_intent = $this->create_and_confirm_setup_intent(); $setup_intent_output = [ 'id' => $setup_intent->get_id(), 'status' => $setup_intent->get_status(), 'client_secret' => $setup_intent->get_client_secret(), ]; wp_send_json_success( $setup_intent_output, 200 ); } catch ( Exception $e ) { // Send back error so it can be displayed to the customer. wp_send_json_error( [ 'error' => [ 'message' => WC_Payments_Utils::get_filtered_error_message( $e ), ], ], WC_Payments_Utils::get_filtered_error_status_code( $e ) ); } } /** * Returns a formatted token list for a user. * * @param int $user_id The user ID. */ protected function get_user_formatted_tokens_array( $user_id ) { $tokens = WC_Payment_Tokens::get_tokens( [ 'user_id' => $user_id, 'gateway_id' => self::GATEWAY_ID, 'limit' => self::USER_FORMATTED_TOKENS_LIMIT, ] ); return array_map( static function ( WC_Payment_Token $token ): array { return [ 'tokenId' => $token->get_id(), 'paymentMethodId' => $token->get_token(), 'isDefault' => $token->get_is_default(), 'displayName' => $token->get_display_name(), ]; }, array_values( $tokens ) ); } /** * Checks whether the gateway is enabled. * * @return bool The result. */ public function is_enabled() { return 'yes' === $this->get_option( 'enabled' ); } /** * Disables gateway. */ public function disable() { $this->update_option( 'enabled', 'no' ); } /** * Enables gateway. */ public function enable() { $this->update_option( 'enabled', 'yes' ); } /** * Returns the list of enabled payment method types for UPE. * * @return string[] */ public function get_upe_enabled_payment_method_ids() { return $this->get_option( 'upe_enabled_payment_method_ids', [ Payment_Method::CARD, ] ); } /** * Returns the list of statuses and capabilities available for UPE payment methods in the cached account. * * @return mixed[] The payment method statuses. */ public function get_upe_enabled_payment_method_statuses() { $account_data = $this->account->get_cached_account_data(); $capabilities = $account_data['capabilities'] ?? []; $requirements = $account_data['capability_requirements'] ?? []; $statuses = []; if ( $capabilities ) { foreach ( $capabilities as $capability_id => $status ) { $statuses[ $capability_id ] = [ 'status' => $status, 'requirements' => $requirements[ $capability_id ] ?? [], ]; } } return 0 === count( $statuses ) ? [ 'card_payments' => [ 'status' => 'active', 'requirements' => [], ], ] : $statuses; } /** * Returns the mapping list between capability keys and payment type keys * * @return string[] */ public function get_payment_method_capability_key_map(): array { return $this->payment_method_capability_key_map; } /** * Updates the account cache with the new payment method status, until it gets fetched again from the server. * * @return void */ public function refresh_cached_account_data() { $this->account->refresh_account_data(); } /** * Updates the cached account data. * * @param string $property Property to update. * @param mixed $data Data to update. */ public function update_cached_account_data( $property, $data ) { $this->account->update_account_data( $property, $data ); } /** * Returns the Stripe payment type of the selected payment method. * * @return string */ public function get_selected_stripe_payment_type_id() { return $this->stripe_id; } /** * Returns the list of enabled payment method types for UPE that are available based on the manual capture setting. * * @return string[] */ public function get_upe_enabled_payment_method_ids_based_on_manual_capture() { $automatic_capture = empty( $this->get_option( 'manual_capture' ) ) || $this->get_option( 'manual_capture' ) === 'no'; if ( $automatic_capture ) { return $this->get_upe_enabled_payment_method_ids(); } return array_intersect( $this->get_upe_enabled_payment_method_ids(), [ Payment_Method::CARD, Payment_Method::LINK ] ); } /** * Returns the list of enabled payment method types that will function with the current checkout. * * @param string $order_id optional Order ID. * @param bool $force_currency_check optional Whether the currency check is required even if is_admin(). * * @return string[] */ public function get_payment_method_ids_enabled_at_checkout( $order_id = null, $force_currency_check = false ) { $upe_enabled_payment_methods = $this->get_upe_enabled_payment_method_ids_based_on_manual_capture(); if ( is_wc_endpoint_url( 'order-pay' ) ) { $force_currency_check = true; } $enabled_payment_methods = []; $active_payment_methods = $this->get_upe_enabled_payment_method_statuses(); foreach ( $upe_enabled_payment_methods as $payment_method_id ) { $payment_method_capability_key = $this->payment_method_capability_key_map[ $payment_method_id ] ?? 'undefined_capability_key'; if ( isset( $this->payment_methods[ $payment_method_id ] ) ) { // When creating a payment intent, we need to ensure the currency is matching // with the payment methods which are sent with the payment intent request, otherwise // Stripe returns an error. // In order to allow payment methods to be displayed in admin pages (e.g. blocks editor), // we need to skip the currency check (unless force_currency_check is true). // force_currency_check = 0 is_admin = 0 -> skip_currency_check = 0. // force_currency_check = 0 is_admin = 1 -> skip_currency_check = 1. // force_currency_check = 1 is_admin = 0 -> skip_currency_check = 0. // force_currency_check = 1 is_admin = 1 -> skip_currency_check = 0. $skip_currency_check = ! $force_currency_check && is_admin(); $processing_payment_method = $this->payment_methods[ $payment_method_id ]; if ( $processing_payment_method->is_enabled_at_checkout( $this->get_account_country(), $skip_currency_check ) && ( $skip_currency_check || $processing_payment_method->is_currency_valid( $this->get_account_domestic_currency(), $order_id ) ) ) { $status = $active_payment_methods[ $payment_method_capability_key ]['status'] ?? null; if ( 'active' === $status ) { $enabled_payment_methods[] = $payment_method_id; } } } } // if credit card payment method is not enabled, we don't use stripe link. if ( ! in_array( CC_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $enabled_payment_methods, true ) && in_array( LinkDefinition::get_id(), $enabled_payment_methods, true ) ) { $link_stripe_id = LinkDefinition::get_id(); $enabled_payment_methods = array_filter( $enabled_payment_methods, static function ( $method ) use ( $link_stripe_id ) { return $link_stripe_id !== $method; } ); } return $enabled_payment_methods; } /** * Returns the list of enabled payment method types that will function with the current checkout filtered by fees. * * @param string $order_id optional Order ID. * @param bool $force_currency_check optional Whether the currency check is required even if is_admin(). * @return string[] */ public function get_payment_method_ids_enabled_at_checkout_filtered_by_fees( $order_id = null, $force_currency_check = false ) { $enabled_payment_methods = $this->get_payment_method_ids_enabled_at_checkout( $order_id, $force_currency_check ); $methods_with_fees = array_keys( $this->account->get_fees() ); return array_values( array_intersect( $enabled_payment_methods, $methods_with_fees ) ); } /** * Returns the list of available payment method types for UPE. * See https://stripe.com/docs/stripe-js/payment-element#web-create-payment-intent for a complete list. * * @return string[] */ public function get_upe_available_payment_methods() { $available_methods = [ 'card' ]; // This gets all the registered payment method definitions. As new payment methods are converted from the legacy style, they need to be removed from the list above. $payment_method_definitions = PaymentMethodDefinitionRegistry::instance()->get_all_payment_method_definitions(); foreach ( $payment_method_definitions as $definition_class ) { $available_methods[] = $definition_class::get_id(); } $available_methods = array_values( apply_filters( 'wcpay_upe_available_payment_methods', $available_methods ) ); $methods_with_fees = array_keys( $this->account->get_fees() ); // Google Pay and Apple Pay use card payment fees, so if card is available, they should be too. if ( in_array( 'card', $methods_with_fees, true ) ) { if ( in_array( 'google_pay', $available_methods, true ) ) { $methods_with_fees[] = 'google_pay'; } if ( in_array( 'apple_pay', $available_methods, true ) ) { $methods_with_fees[] = 'apple_pay'; } } return array_values( array_intersect( $available_methods, $methods_with_fees ) ); } /** * Handle AJAX request for saving UPE appearance value to transient. * * @throws Exception - If nonce or setup intent is invalid. */ public function save_upe_appearance_ajax() { try { $is_nonce_valid = check_ajax_referer( 'wcpay_save_upe_appearance_nonce', false, false ); if ( ! $is_nonce_valid ) { throw new Exception( __( 'Unable to update UPE appearance values at this time.', 'woocommerce-payments' ) ); } $elements_location = isset( $_POST['elements_location'] ) ? wc_clean( wp_unslash( $_POST['elements_location'] ) ) : null; $appearance = isset( $_POST['appearance'] ) ? json_decode( wc_clean( wp_unslash( $_POST['appearance'] ) ) ) : null; $valid_locations = [ 'blocks_checkout', 'shortcode_checkout', 'bnpl_product_page', 'bnpl_classic_cart', 'bnpl_cart_block', 'add_payment_method' ]; if ( ! $elements_location || ! in_array( $elements_location, $valid_locations, true ) ) { throw new Exception( __( 'Unable to update UPE appearance values at this time.', 'woocommerce-payments' ) ); } if ( in_array( $elements_location, [ 'blocks_checkout', 'shortcode_checkout' ], true ) ) { $is_blocks_checkout = 'blocks_checkout' === $elements_location; /** * This filter is only called on "save" of the appearance, to avoid calling it on every page load. * If you apply changes through this filter, you'll need to clear the transient data to see them at checkout. * * @deprecated 7.4.0 Use {@see 'wcpay_elements_appearance'} instead. * @since 7.3.0 */ $appearance = apply_filters_deprecated( 'wcpay_upe_appearance', [ $appearance, $is_blocks_checkout ], '7.4.0', 'wcpay_elements_appearance' ); } /** * This filter is only called on "save" of the appearance, to avoid calling it on every page load. * If you apply changes through this filter, you'll need to clear the transient data to see them at checkout. * $elements_location can be 'blocks_checkout', 'shortcode_checkout', 'bnpl_product_page', 'bnpl_classic_cart', 'bnpl_cart_block', 'add_payment_method'. * * @since 7.4.0 */ $appearance = apply_filters( 'wcpay_elements_appearance', $appearance, $elements_location ); $appearance_transient = [ 'shortcode_checkout' => self::UPE_APPEARANCE_TRANSIENT, 'add_payment_method' => self::UPE_ADD_PAYMENT_METHOD_APPEARANCE_TRANSIENT, 'blocks_checkout' => self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT, 'bnpl_product_page' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT, 'bnpl_classic_cart' => self::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT, 'bnpl_cart_block' => self::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT, ][ $elements_location ]; $appearance_theme_transient = [ 'shortcode_checkout' => self::UPE_APPEARANCE_THEME_TRANSIENT, 'add_payment_method' => self::UPE_ADD_PAYMENT_METHOD_APPEARANCE_THEME_TRANSIENT, 'blocks_checkout' => self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT, 'bnpl_product_page' => self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT, 'bnpl_classic_cart' => self::UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT, 'bnpl_cart_block' => self::UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT, ][ $elements_location ]; if ( null !== $appearance ) { set_transient( $appearance_transient, $appearance, DAY_IN_SECONDS ); set_transient( $appearance_theme_transient, $appearance->theme, DAY_IN_SECONDS ); } wp_send_json_success( $appearance, 200 ); } catch ( Exception $e ) { // Send back error so it can be displayed to the customer. wp_send_json_error( [ 'error' => [ 'message' => WC_Payments_Utils::get_filtered_error_message( $e ), ], ], WC_Payments_Utils::get_filtered_error_status_code( $e ) ); } } /** * Clear the saved UPE appearance transient value. */ public function clear_upe_appearance_transient() { delete_transient( self::UPE_APPEARANCE_TRANSIENT ); delete_transient( self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT ); delete_transient( self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT ); delete_transient( self::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT ); delete_transient( self::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT ); delete_transient( self::UPE_APPEARANCE_THEME_TRANSIENT ); delete_transient( self::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT ); delete_transient( self::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_THEME_TRANSIENT ); delete_transient( self::UPE_BNPL_CLASSIC_CART_APPEARANCE_THEME_TRANSIENT ); delete_transient( self::UPE_BNPL_CART_BLOCK_APPEARANCE_THEME_TRANSIENT ); } /** * Returns true if the code returned from the API represents an error that should be rate-limited. * * @param string $error_code The error code returned from the API. * * @return bool Whether the rate limiter should be bumped. */ protected function should_bump_rate_limiter( string $error_code ): bool { return in_array( $error_code, [ 'card_declined', 'incorrect_number', 'incorrect_cvc' ], true ); } /** * Returns boolean for whether payment gateway supports saved payments. * * @return bool True, if gateway supports saved payments. False, otherwise. */ public function should_support_saved_payments() { return $this->is_enabled_for_saved_payments( $this->stripe_id ); } /** * Returns true when viewing payment methods page. * * @return bool */ private function is_payment_methods_page() { global $wp; $page_id = wc_get_page_id( 'myaccount' ); return ( $page_id && is_page( $page_id ) && ( isset( $wp->query_vars['payment-methods'] ) ) ); } /** * Verifies that the proper intent is used to process the order. * * @param WC_Order $order The order object based on the order_id received from the request. * @param string $intent_id_from_request The intent ID received from the request. * * @return bool True if the proper intent is used to process the order, false otherwise. */ public function is_proper_intent_used_with_order( $order, $intent_id_from_request ) { $intent_id_attached_to_order = $this->order_service->get_intent_id_for_order( $order ); if ( ! hash_equals( $intent_id_attached_to_order, $intent_id_from_request ) ) { Logger::error( sprintf( 'Intent ID mismatch. Received in request: %1$s. Attached to order: %2$s. Order ID: %3$d', $intent_id_from_request, $intent_id_attached_to_order, $order->get_id() ) ); return false; } return true; } /** * True if the request contains the values that indicates a redirection after a successful setup intent creation. * * @return bool */ public function is_setup_intent_success_creation_redirection() { return ! empty( $_GET['setup_intent_client_secret'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended ! empty( $_GET['setup_intent'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended ! empty( $_GET['redirect_status'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended 'succeeded' === $_GET['redirect_status']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended } /** * Function to be used with array_filter * to filter UPE payment methods that support saved payments * * @param string $payment_method_id Stripe payment method. * * @return bool */ public function is_enabled_for_saved_payments( $payment_method_id ) { $payment_method = $this->get_selected_payment_method( $payment_method_id ); if ( ! $payment_method ) { return false; } return $payment_method->is_reusable() && ( is_admin() || $payment_method->is_currency_valid( $this->get_account_domestic_currency() ) ); } /** * Gets UPE_Payment_Method instance from ID. * * @param string $payment_method_type Stripe payment method type ID. * @return UPE_Payment_Method|false UPE payment method instance. */ public function get_selected_payment_method( $payment_method_type ) { return WC_Payments::get_payment_method_by_id( $payment_method_type ); } /** * Return the payment method type from the payment method details. * * @param array $payment_method_details Payment method details. * @return string|null Payment method type or nothing. */ private function get_payment_method_type_from_payment_details( $payment_method_details ) { return $payment_method_details['type'] ?? null; } /** * Get the payment method used with a setup intent. * * @param WC_Payments_API_Setup_Intention $intent The PaymentIntent object. * @param WC_Payment_Token $token The payment token. * @return string|null The payment method type. */ private function get_payment_method_type_for_setup_intent( $intent, $token ) { return 'wcpay_link' !== $token->get_type() ? $intent->get_payment_method_type() : LinkDefinition::get_id(); } /** * This function wraps WC_Payments::get_payment_method_map, useful for unit testing. * * @return array Array of UPE_Payment_Method instances. */ public function wc_payments_get_payment_method_map() { return WC_Payments::get_payment_method_map(); } /** * Returns the payment methods for this gateway. * * @return array|UPE_Payment_Method[] */ public function get_payment_methods() { return $this->payment_methods; } /** * Returns the UPE payment method for the gateway. * * @return UPE_Payment_Method */ public function get_payment_method() { return $this->payment_method; } /** * Returns Stripe payment method type ID. * * @return string */ public function get_stripe_id() { return $this->stripe_id; } /** * This function wraps WC_Payments::get_payment_gateway_by_id, useful for unit testing. * * @param string $payment_method_id Stripe payment method type ID. * @return false|WC_Payment_Gateway_WCPay Matching UPE Payment Gateway instance. */ public function wc_payments_get_payment_gateway_by_id( $payment_method_id ) { return WC_Payments::get_payment_gateway_by_id( $payment_method_id ); } /** * This function wraps WC_Payments::get_payment_method_by_id, useful for unit testing. * * @param string $payment_method_id Stripe payment method type ID. * @return false|UPE_Payment_Method Matching UPE Payment Method instance. */ public function wc_payments_get_payment_method_by_id( $payment_method_id ) { return WC_Payments::get_payment_method_by_id( $payment_method_id ); } /** * Checks if UPE appearance theme is set and returns appropriate icon URL. * * @return string */ public function get_theme_icon() { $upe_appearance_theme = get_transient( self::UPE_APPEARANCE_THEME_TRANSIENT ); if ( $upe_appearance_theme ) { return 'night' === $upe_appearance_theme ? $this->payment_method->get_dark_icon() : $this->payment_method->get_icon(); } return $this->payment_method->get_icon(); } /** * Get the gateway's method description. * Returns payment method specific description for non-card gateways, * or the general WooPayments description for the main card gateway. * * @return string */ public function get_method_description() { // For non-card payment methods, return the specific payment method description. if ( 'card' !== $this->stripe_id ) { return $this->payment_method->get_description( $this->get_account_country() ); } // For the main card gateway, return the general WooPayments description. $description = sprintf( /* translators: %1$s: WooPayments */ __( '%1$s gives your store flexibility to accept credit cards, debit cards, and Apple Pay. Enable popular local payment methods and other digital wallets like Google Pay to give customers even more choice.', 'woocommerce-payments' ), 'WooPayments' ); if ( WooPay_Utilities::is_store_country_available() ) { $description = sprintf( /* translators: %s: WooPay, */ __( 'Payments made simple — including %s, a new express checkout feature.', 'woocommerce-payments' ), 'WooPay' ); } return $description; } /** * Calls duplicate payment methods detection service to find duplicates. * This method acts as a wrapper. The approach should be reverted once * https://github.com/Automattic/woocommerce-payments/issues/7464 is resolved. */ public function find_duplicates() { return $this->duplicate_payment_methods_detection_service->find_duplicates(); } /** * Get the recommended payment methods list. * * @param string $country_code Optional. The business location country code. Provide a 2-letter ISO country code. * If not provided, the account country will be used if the account is connected. * Otherwise, the store's base country will be used. * * @return array List of recommended payment methods for the given country. * Empty array if there are no recommendations available. * Each item in the array should be an associative array with at least the following entries: * - @string id: The payment method ID. * - @string title: The payment method title/name. * - @bool enabled: Whether the payment method is enabled. * - @int order/priority: The order/priority of the payment method. */ public function get_recommended_payment_methods( string $country_code = '' ): array { if ( empty( $country_code ) ) { // If the account is connected, use the account country. if ( $this->account->is_provider_connected() ) { $country_code = $this->get_account_country(); } else { // If the account is not connected, use the store's base country. $country_code = WC()->countries->get_base_country(); } } return $this->account->get_recommended_payment_methods( $country_code ); } /** * Determine whether redirection is needed for the non-card UPE payment method. * * @param array $payment_methods The list of payment methods used for the order processing, usually consists of one method only. * @return boolean True if the array contains a redirect-based payment method. False otherwise. */ private function upe_needs_redirection( $payment_methods ) { // Payment methods that require redirect (customer leaves the site to authorize). $redirect_payment_methods = [ 'amazon_pay' ]; // Check if any redirect-based payment method is in the array. if ( array_intersect( $redirect_payment_methods, $payment_methods ) ) { return true; } // Original logic: single non-card payment method. return 1 === count( $payment_methods ) && 'card' !== $payment_methods[0]; } /** * Handles the shipping requirement for Afterpay payments. * * This method extracts the shipping and billing data from the order and sets the appropriate * shipping data for the Afterpay payment request. If neither shipping nor billing data is valid * for shipping, an exception is thrown. * * @param WC_Order $order The order object containing shipping and billing information. * @param Create_And_Confirm_Intention $request The Afterpay payment request object to set shipping data on. * * @throws Invalid_Address_Exception If neither shipping nor billing address is valid for Afterpay payments. * @return void */ private function handle_afterpay_shipping_requirement( WC_Order $order, Create_And_Confirm_Intention $request ): void { $shipping_data = $this->retrieve_usable_shipping_data_from_order( $order ); if ( $shipping_data ) { $request->set_shipping( $shipping_data ); return; } throw new Invalid_Address_Exception( __( 'A valid shipping address is required for Afterpay payments.', 'woocommerce-payments' ) ); } /** * Retrieves the usable shipping data from the order (either from the shipping or billing details). * Returns null if there are no usable addresses. * * @param WC_Order $order The order object. * @return null|array */ private function retrieve_usable_shipping_data_from_order( WC_Order $order ): ?array { $wc_locale_data = WC()->countries->get_country_locale(); $check_if_usable = function ( array $address ) use ( $wc_locale_data ): bool { if ( ! isset( $address['country'] ) ) { return false; } if ( $address['country'] && isset( $wc_locale_data[ $address['country'] ] ) ) { $country_locale_data = $wc_locale_data[ $address['country'] ]; $fields_to_check = [ 'state' => 'state', 'city' => 'city', 'postcode' => 'postal_code', 'address_1' => 'line1', ]; foreach ( $fields_to_check as $locale_field => $address_field ) { $is_field_required = ( ! isset( $country_locale_data[ $locale_field ] ) || ! isset( $country_locale_data[ $locale_field ]['required'] ) || $country_locale_data[ $locale_field ]['required'] ); if ( $is_field_required && ! $address[ $address_field ] ) { return false; } } return true; } return $address['country'] && $address['state'] && $address['city'] && $address['postal_code'] && $address['line1']; }; $shipping_data = $this->order_service->get_shipping_data_from_order( $order ); if ( $check_if_usable( $shipping_data['address'] ) ) { return $shipping_data; } // Afterpay fails if we send more parameters than expected in the // shipping address. This ensures that we only send the name and address // fields, as in get_shipping_data_from_order. $billing_data = $this->order_service->get_billing_data_from_order( $order ); if ( $check_if_usable( $billing_data['address'] ) ) { return [ 'name' => $billing_data['name'] ?? '', 'address' => $billing_data['address'] ?? [], ]; } return null; } /** * Modifies the create intent parameters when processing a payment. * * If the selected Stripe payment type is AFTERPAY, it updates the shipping data in the request. * * @param Create_And_Confirm_Intention $request The request object for creating and confirming intention. * @param Payment_Information $payment_information The payment information object. * @param WC_Order $order The order object. * @throws Invalid_Address_Exception * * @return void */ protected function modify_create_intent_parameters_when_processing_payment( Create_And_Confirm_Intention $request, Payment_Information $payment_information, WC_Order $order ): void { if ( Payment_Method::AFTERPAY === $this->get_selected_stripe_payment_type_id() ) { $this->handle_afterpay_shipping_requirement( $order, $request ); } } } includes/class-wc-payments-account.php 0000777 00000341770 15251156605 0014120 0 ustar 00 <?php /** * Class WC_Payments_Account * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\Core\Server\Request\Get_Account; use WCPay\Core\Server\Request; use WCPay\Core\Server\Request\Update_Account; use WCPay\Exceptions\API_Exception; use WCPay\Logger; use WCPay\Database_Cache; use WCPay\MultiCurrency\Interfaces\MultiCurrencyAccountInterface; /** * Class handling any account connection functionality */ class WC_Payments_Account implements MultiCurrencyAccountInterface { // ACCOUNT_OPTION is only used in the supporting dev tools plugin, it can be removed once everyone has upgraded. const ACCOUNT_OPTION = 'wcpay_account_data'; const ONBOARDING_DISABLED_TRANSIENT = 'wcpay_on_boarding_disabled'; const ONBOARDING_STATE_TRANSIENT = 'wcpay_stripe_onboarding_state'; const WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT = 'woopay_enabled_by_default'; const ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT = 'test_drive_account_settings_for_live_account'; const EMBEDDED_KYC_IN_PROGRESS_OPTION = 'wcpay_onboarding_embedded_kyc_in_progress'; const ERROR_MESSAGE_TRANSIENT = 'wcpay_error_message'; const INSTANT_DEPOSITS_REMINDER_ACTION = 'wcpay_instant_deposit_reminder'; const TRACKS_EVENT_ACCOUNT_CONNECT_START = 'wcpay_account_connect_start'; const TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_START = 'wcpay_account_connect_wpcom_connection_start'; const TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_SUCCESS = 'wcpay_account_connect_wpcom_connection_success'; const TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_FAILURE = 'wcpay_account_connect_wpcom_connection_failure'; const TRACKS_EVENT_ACCOUNT_CONNECT_FINISHED = 'wcpay_account_connect_finished'; const TRACKS_EVENT_KYC_REMINDER_MERCHANT_RETURNED = 'wcpay_kyc_reminder_merchant_returned'; const TRACKS_EVENT_ACCOUNT_REFERRAL = 'wcpay_account_referral'; // NOX-related constants from the WooCommerce core. const NOX_PROFILE_OPTION_KEY = 'woocommerce_woopayments_nox_profile'; const NOX_ONBOARDING_LOCKED_KEY = 'woocommerce_woopayments_nox_onboarding_locked'; const STORE_SETUP_SYNC_ACTION = 'wcpay_store_setup_sync'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * Cache util for managing the account data * * @var Database_Cache */ private $database_cache; /** * Action scheduler service * * @var WC_Payments_Action_Scheduler_Service */ private $action_scheduler_service; /** * WC_Payments_Onboarding_Service instance for working with onboarding business logic * * @var WC_Payments_Onboarding_Service */ private $onboarding_service; /** * WC_Payments_Redirect_Service instance for handling redirects business logic * * @var WC_Payments_Redirect_Service */ private $redirect_service; /** * Class constructor * * @param WC_Payments_API_Client $payments_api_client Payments API client. * @param Database_Cache $database_cache Database cache util. * @param WC_Payments_Action_Scheduler_Service $action_scheduler_service Action scheduler service. * @param WC_Payments_Onboarding_Service $onboarding_service Onboarding service. * @param WC_Payments_Redirect_Service $redirect_service Redirect service. */ public function __construct( WC_Payments_API_Client $payments_api_client, Database_Cache $database_cache, WC_Payments_Action_Scheduler_Service $action_scheduler_service, WC_Payments_Onboarding_Service $onboarding_service, WC_Payments_Redirect_Service $redirect_service ) { $this->payments_api_client = $payments_api_client; $this->database_cache = $database_cache; $this->action_scheduler_service = $action_scheduler_service; $this->onboarding_service = $onboarding_service; $this->redirect_service = $redirect_service; } /** * Initialise class hooks. * * @return void */ public function init_hooks() { // Add admin init hooks. // Our onboarding handling comes first. add_action( 'admin_init', [ $this, 'maybe_handle_onboarding' ] ); add_action( 'admin_init', [ $this, 'maybe_activate_woopay' ] ); // Second, handle redirections based on context. add_action( 'admin_init', [ $this, 'maybe_redirect_after_plugin_activation' ], 11 ); // Run this after the WC setup wizard and onboarding redirection logic. add_action( 'admin_init', [ $this, 'maybe_redirect_by_get_param' ], 12 ); // Run this after the redirect to onboarding logic. // Third, handle page redirections. add_action( 'admin_init', [ $this, 'maybe_redirect_onboarding_referral' ], 13 ); add_action( 'admin_init', [ $this, 'maybe_redirect_from_settings_page' ], 15 ); add_action( 'admin_init', [ $this, 'maybe_redirect_from_onboarding_wizard_page' ], 15 ); add_action( 'admin_init', [ $this, 'maybe_redirect_from_connect_page' ], 15 ); add_action( 'admin_init', [ $this, 'maybe_redirect_from_overview_page' ], 15 ); // Add handlers for inbox notes and reminders. add_action( 'woocommerce_payments_account_refreshed', [ $this, 'handle_instant_deposits_inbox_note' ] ); add_action( 'woocommerce_payments_account_refreshed', [ $this, 'handle_loan_approved_inbox_note' ] ); add_action( self::INSTANT_DEPOSITS_REMINDER_ACTION, [ $this, 'handle_instant_deposits_inbox_reminder' ] ); // Add all other hooks. add_filter( 'allowed_redirect_hosts', [ $this, 'allowed_redirect_hosts' ] ); add_action( 'jetpack_site_registered', [ $this, 'clear_cache' ] ); add_action( 'updated_option', [ $this, 'possibly_update_wcpay_account_locale' ], 10, 3 ); add_action( 'woocommerce_woocommerce_payments_updated', [ $this, 'clear_cache' ] ); // Hook into the recurring store setup sync action and do the store setup sync. add_action( self::STORE_SETUP_SYNC_ACTION, [ $this, 'store_setup_sync' ] ); // Also do a store setup sync when the client is updated to a new version. add_action( 'woocommerce_woocommerce_payments_updated', [ $this, 'store_setup_sync' ] ); } /** * Wipes the account data option, forcing to re-fetch the account data from WP.com. */ public function clear_cache() { $this->database_cache->delete( Database_Cache::ACCOUNT_KEY ); } /** * Return connected account ID * * @return string|null Account ID if connected, null if not connected or on error */ public function get_stripe_account_id() { $account = $this->get_cached_account_data(); if ( empty( $account ) ) { return null; } return $account['account_id']; } /** * Gets public key for the connected account * * @param bool $is_test true to get the test key, false otherwise. * * @return string|null public key if connected, null if not connected. */ public function get_publishable_key( $is_test ) { $account = $this->get_cached_account_data(); if ( empty( $account ) ) { return null; } if ( $is_test ) { return $account['test_publishable_key']; } return $account['live_publishable_key']; } /** * Checks if the account is connected to the payment provider. * Note: This method is a proxy for `is_stripe_connected` for the MultiCurrencyAccountInterface. * * @param bool $on_error Value to return on server error, defaults to false. * * @return bool True if the account is connected, false otherwise, $on_error on error. */ public function is_provider_connected( bool $on_error = false ): bool { return $this->is_stripe_connected( $on_error ); } /** * Determine if the store has a working Jetpack connection. * * @return bool Whether the Jetpack connection is established and working or not. */ public function has_working_jetpack_connection(): bool { return $this->payments_api_client->is_server_connected() && $this->payments_api_client->has_server_connection_owner(); } /** * Check if there is meaningful data in the WooPayments account cache. * * It bypasses WPCOM/Jetpack connection check, the cache expiry check and only checks if the account_id is present. * * @return boolean Whether there is account data. */ public function has_account_data(): bool { $account_data = $this->database_cache->get( Database_Cache::ACCOUNT_KEY, true ); if ( ! empty( $account_data['account_id'] ) ) { return true; } return false; } /** * Checks if the account is connected, assumes the value of $on_error on server error. * * @param bool $on_error Value to return on server error, defaults to false. * * @return bool True if the account is connected, false otherwise, $on_error on error. */ public function is_stripe_connected( bool $on_error = false ): bool { try { return $this->try_is_stripe_connected(); } catch ( Exception $e ) { return $on_error; } } /** * Checks if the account is connected, throws on server error. * * @return bool True if the account is connected, false otherwise. * @throws Exception Throws exception when unable to detect connection status. */ public function try_is_stripe_connected(): bool { $account = $this->get_cached_account_data(); if ( false === $account ) { throw new Exception( esc_html__( 'Failed to detect connection status', 'woocommerce-payments' ) ); } // The empty array indicates that account is not connected yet. return [] !== $account; } /** * Checks if the account is valid. * * This means: * - it's connected (i.e. we have account data) * - has submitted details (i.e. is not partially onboarded) * - has valid card_payments capability status (requested, pending_verification, active and other valid ones). * * Card_payments capability is crucial for account to function properly. If it is unrequested, we shouldn't show * any other options for the merchants since it'll lead to various errors. * * @see https://github.com/Automattic/woocommerce-payments/issues/5275 * * @return bool True if the account is a valid Stripe account, false otherwise. */ public function is_stripe_account_valid(): bool { $account = $this->get_cached_account_data(); // The account is disconnected or we failed to get the account data. if ( empty( $account ) ) { return false; } // The account is partially onboarded. if ( empty( $account['details_submitted'] ) ) { return false; } // The account doesn't have the minimum required capabilities. if ( ! isset( $account['capabilities']['card_payments'] ) || 'unrequested' === $account['capabilities']['card_payments'] ) { return false; } // The account is valid. return true; } /** * Checks if the account has been rejected, assumes the value of false on any account retrieval error. * Returns false if the account is not connected. * * @return bool True if the account is connected and rejected, false otherwise or on error. */ public function is_account_rejected(): bool { if ( ! $this->is_stripe_connected() ) { return false; } $account = $this->get_cached_account_data(); return strpos( $account['status'] ?? '', 'rejected' ) === 0; } /** * Checks if the account is under review, assumes the value of false on any account retrieval error. * Returns false if the account is not connected. * * @return bool */ public function is_account_under_review(): bool { if ( ! $this->is_stripe_connected() ) { return false; } $account = $this->get_cached_account_data(); return 'under_review' === ( $account['status'] ?? false ); } /** * Checks if the account "details_submitted" flag is true. * This is a proxy for telling if an account has completed onboarding. * If the "details_submitted" flag is false, it means that the account has not * yet finished the initial KYC. * * @return boolean True if the account is connected and details are not submitted, false otherwise. */ public function is_details_submitted(): bool { $account = $this->get_cached_account_data(); $details_submitted = $account['details_submitted'] ?? false; return true === $details_submitted; } /** * Gets the account status data for rendering on the settings page. * * @return array An array containing the status data, or [ 'error' => true ] on error or no connected account. */ public function get_account_status_data(): array { $account = $this->get_cached_account_data(); if ( empty( $account ) ) { // empty means no account. This data should not be used when the account is not connected. return [ 'error' => true, ]; } if ( ! isset( $account['status'], $account['payments_enabled'] ) ) { // return an error if any of the account data is missing. return [ 'error' => true, ]; } return [ 'email' => $account['email'] ?? '', 'country' => $account['country'] ?? Country_Code::UNITED_STATES, 'status' => $account['status'], 'created' => $account['created'] ?? '', 'testDrive' => $account['is_test_drive'] ?? false, 'isLive' => $account['is_live'] ?? false, 'paymentsEnabled' => $account['payments_enabled'], 'detailsSubmitted' => $account['details_submitted'] ?? true, 'deposits' => $account['deposits'] ?? [], 'currentDeadline' => $account['current_deadline'] ?? false, 'pastDue' => $account['has_overdue_requirements'] ?? false, // Test-drive accounts don't have access to the Stripe dashboard. 'accountLink' => empty( $account['is_test_drive'] ) ? $this->get_login_url() : false, 'hasSubmittedVatData' => $account['has_submitted_vat_data'] ?? false, 'isDocumentsEnabled' => $account['is_documents_enabled'] ?? false, 'communicationsEmail' => $account['communications_email'] ?? '', 'requirements' => [ 'errors' => $account['requirements']['errors'] ?? [], ], 'fraudProtection' => [ 'declineOnAVSFailure' => $account['fraud_mitigation_settings']['avs_check_enabled'] ?? null, 'declineOnCVCFailure' => $account['fraud_mitigation_settings']['cvc_check_enabled'] ?? null, ], // Campaigns are temporary flags that are used to enable/disable features for a limited time. 'campaigns' => [ // The flag for the WordPress.org merchant review campaign in 2025. Eligibility is determined per-account on transact-platform-server. 'wporgReview2025' => $account['eligibility_wporg_review_campaign_2025'] ?? false, ], ]; } /** * Get structured account details including status, payout info, and banners for frontend display. * * @return null|array Account details array with nested status objects, or null if unavailable. */ public function get_account_details(): ?array { $account = $this->get_cached_account_data(); if ( empty( $account['account_details'] ) ) { return null; } $account_details = $account['account_details']; $is_valid = is_array( $account_details ) && isset( $account_details['account_status'], $account_details['payout_status'] ) && array_key_exists( 'banner', $account_details ); return $is_valid ? $account_details : null; } /** * Gets the account statement descriptor for rendering on the settings page. * * @return string Account statement descriptor. */ public function get_statement_descriptor(): string { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['statement_descriptor'] ) ? $account['statement_descriptor'] : ''; } /** * Gets the account statement descriptor for rendering on the settings page. * * @return string Account statement descriptor. */ public function get_statement_descriptor_kanji(): string { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['statement_descriptor_kanji'] ) ? $account['statement_descriptor_kanji'] : ''; } /** * Gets the account statement descriptor for rendering on the settings page. * * @return string Account statement descriptor. */ public function get_statement_descriptor_kana(): string { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['statement_descriptor_kana'] ) ? $account['statement_descriptor_kana'] : ''; } /** * Gets the business name. * * @return string Business profile name. */ public function get_business_name(): string { $account = $this->get_cached_account_data(); return isset( $account['business_profile']['name'] ) ? $account['business_profile']['name'] : ''; } /** * Gets the business url. * * @return string Business profile url. */ public function get_business_url(): string { $account = $this->get_cached_account_data(); return isset( $account['business_profile']['url'] ) ? $account['business_profile']['url'] : ''; } /** * Gets the business support address. * * @return array Business profile support address. */ public function get_business_support_address(): array { $account = $this->get_cached_account_data(); return isset( $account['business_profile']['support_address'] ) ? $account['business_profile']['support_address'] : []; } /** * Gets the business support email. * * @return string Business profile support email. */ public function get_business_support_email(): string { $account = $this->get_cached_account_data(); return isset( $account['business_profile']['support_email'] ) ? $account['business_profile']['support_email'] : ''; } /** * Gets the business support phone. * * @return string Business profile support phone. */ public function get_business_support_phone(): string { $account = $this->get_cached_account_data(); return isset( $account['business_profile']['support_phone'] ) ? $account['business_profile']['support_phone'] : ''; } /** * Gets the communications email. * * @return string Communications email. */ public function get_communications_email(): string { $account = $this->get_cached_account_data(); return isset( $account['communications_email'] ) ? $account['communications_email'] : ''; } /** * Gets the branding logo. * * @return string branding logo. */ public function get_branding_logo(): string { $account = $this->get_cached_account_data(); return isset( $account['branding']['logo'] ) ? $account['branding']['logo'] : ''; } /** * Gets the branding icon. * * @return string branding icon. */ public function get_branding_icon(): string { $account = $this->get_cached_account_data(); return isset( $account['branding']['icon'] ) ? $account['branding']['icon'] : ''; } /** * Gets the branding primary color. * * @return string branding primary color. */ public function get_branding_primary_color(): string { $account = $this->get_cached_account_data(); return isset( $account['branding']['primary_color'] ) ? $account['branding']['primary_color'] : ''; } /** * Gets the branding secondary color. * * @return string branding secondary color. */ public function get_branding_secondary_color(): string { $account = $this->get_cached_account_data(); return isset( $account['branding']['secondary_color'] ) ? $account['branding']['secondary_color'] : ''; } /** * Gets the deposit schedule interval. * * @return string interval e.g. weekly, monthly. */ public function get_deposit_schedule_interval(): string { $account = $this->get_cached_account_data(); return $account['deposits']['interval'] ?? ''; } /** * Gets the deposit schedule weekly anchor. * * @return string weekly anchor e.g. monday, tuesday. */ public function get_deposit_schedule_weekly_anchor(): string { $account = $this->get_cached_account_data(); return $account['deposits']['weekly_anchor'] ?? ''; } /** * Gets the deposit schedule monthly anchor. * * @return int|null monthly anchor e.g. 1, 2. */ public function get_deposit_schedule_monthly_anchor() { $account = $this->get_cached_account_data(); return ! empty( $account['deposits']['monthly_anchor'] ) ? $account['deposits']['monthly_anchor'] : null; } /** * Gets the number of days payments are delayed for. * * @return int|null e.g. 2, 7. */ public function get_deposit_delay_days() { $account = $this->get_cached_account_data(); return $account['deposits']['delay_days'] ?? null; } /** * Gets the deposit status * * @return string e.g. disabled, blocked, enabled. */ public function get_deposit_status(): string { $account = $this->get_cached_account_data(); return $account['deposits']['status'] ?? ''; } /** * Gets the deposit restrictions * * @return string e.g. not_blocked, blocked, schedule locked. */ public function get_deposit_restrictions(): string { $account = $this->get_cached_account_data(); return $account['deposits']['restrictions'] ?? ''; } /** * Gets whether the account has completed the deposit waiting period. * * @return bool */ public function get_deposit_completed_waiting_period(): bool { $account = $this->get_cached_account_data(); return $account['deposits']['completed_waiting_period'] ?? false; } /** * Get card present eligible flag account * * @return bool */ public function is_card_present_eligible(): bool { $account = $this->get_cached_account_data(); return $account['card_present_eligible'] ?? false; } /** * Get has account connected readers flag * * @return bool */ public function has_card_readers_available(): bool { $account = $this->get_cached_account_data(); return $account['has_card_readers_available'] ?? false; } /** * Gets the current account fees for rendering on the settings page. * * @return array Fees. */ public function get_fees(): array { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['fees'] ) ? $account['fees'] : []; } /** * Gets the current account loan data for rendering on the settings pages. * * @return array loan data. */ public function get_capital() { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['capital'] ) && ! empty( $account['capital'] ) ? $account['capital'] : [ 'loans' => [], 'has_active_loan' => false, 'has_previous_loans' => false, ]; } /** * Gets the current account email for rendering on the settings page. * * @return string Email. */ public function get_account_email(): string { $account = $this->get_cached_account_data(); return $account['email'] ?? ''; } /** * Gets the customer currencies supported by Stripe available for the account. * * @return array Currencies. */ public function get_account_customer_supported_currencies(): array { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['customer_currencies']['supported'] ) ? $account['customer_currencies']['supported'] : []; } /** * List of countries enabled for Stripe platform account. See also this URL: * https://woocommerce.com/document/woopayments/compatibility/countries/#supported-countries * * @return array */ public function get_supported_countries(): array { // This is a wrapper function because of the MultiCurrencyAccountInterface. return WC_Payments_Utils::supported_countries(); } /** * Get the account recommended payment methods to use during onboarding. * * @param string $country_code The account's business location country code. Provide a 2-letter ISO country code. * * @return array List of recommended payment methods for the given country. * Empty array if there are no recommendations, we failed to retrieve recommendations, * or the country is not supported by WooPayments. */ public function get_recommended_payment_methods( string $country_code ): array { // Return early if the country is not supported. if ( ! array_key_exists( $country_code, $this->get_supported_countries() ) ) { return []; } // We use the locale for the current user (defaults to the site locale). $recommended_pms = $this->onboarding_service->get_recommended_payment_methods( $country_code, get_user_locale() ); $recommended_pms = is_array( $recommended_pms ) ? array_values( $recommended_pms ) : []; // Validate the recommended payment methods. // Each must have an ID and a title. $recommended_pms = array_filter( $recommended_pms, function ( $pm ) { return isset( $pm['id'] ) && isset( $pm['title'] ); } ); // Standardize/normalize. // Determine if the payment method should be recommended as enabled. $recommended_pms = array_map( function ( $pm ) { if ( ! isset( $pm['enabled'] ) ) { // Default to enabled since this is a recommended list. $pm['enabled'] = true; // Look at the type, if available, to determine if it should be enabled. if ( isset( $pm['type'] ) ) { $pm['enabled'] = 'available' !== $pm['type']; } } return $pm; }, $recommended_pms ); // Fill in the priority entries with a fallback to the index of the recommendation in the list. $recommended_pms = array_map( function ( $pm, $index ) { if ( ! isset( $pm['priority'] ) ) { $pm['priority'] = $index; } else { $pm['priority'] = intval( $pm['priority'] ); } return $pm; }, $recommended_pms, array_keys( $recommended_pms ) ); return $recommended_pms; } /** * Gets the account live mode value. * * @return bool|null Account is_live value. */ public function get_is_live() { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['is_live'] ) ? $account['is_live'] : null; } /** * Checks if the request contains specific get param to redirect further, and redirects to the relevant link if so. * * Only admins are be able to perform this action. The redirect doesn't happen if the request is an AJAX request. */ public function maybe_redirect_by_get_param() { // Safety check to prevent non-admin users to be redirected to the view offer page. if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return; } // This is an automatic redirection page, used to authenticate users that come from the KYC reminder email. For this reason // we're not using a nonce. The GET parameter accessed here is just to indicate that we should process the redirection. // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['wcpay-connect-redirect'] ) ) { $params = [ 'page' => 'wc-admin', 'path' => '/payments/connect', ]; // We're not in the connect page, don't redirect. if ( count( $params ) !== count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended return; } $redirect_param = sanitize_text_field( wp_unslash( $_GET['wcpay-connect-redirect'] ) ); // Let's record in Tracks merchants returning via the KYC reminder email. if ( 'initial' === $redirect_param ) { $offset = 1; $description = 'initial'; } elseif ( 'second' === $redirect_param ) { $offset = 3; $description = 'second'; } else { $follow_number = in_array( $redirect_param, [ '1', '2', '3', '4' ], true ) ? $redirect_param : '0'; // offset is recorded in days, $follow_number maps to the week number. $offset = (int) $follow_number * 7; $description = 'weekly-' . $follow_number; } $track_props = [ 'offset' => $offset, 'description' => $description, ]; $this->tracks_event( self::TRACKS_EVENT_KYC_REMINDER_MERCHANT_RETURNED, $track_props ); $this->redirect_service->redirect_to_wcpay_connect( 'WCPAY_KYC_REMINDER' ); } // This is an automatic redirection page, used to authenticate users that come from the capitcal offer email. For this reason // we're not using a nonce. The GET parameter accessed here is just to indicate that we should process the redirection. // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['wcpay-loan-offer'] ) ) { $this->redirect_service->redirect_to_capital_view_offer_page(); } // This is an automatic redirection page, used to authenticate users that come from an email link. For this reason // we're not using a nonce. The GET parameter accessed here is just to indicate that we should process the redirection. // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['wcpay-link-handler'] ) ) { // Get all request arguments to be forwarded and remove the link handler identifier. $args = $_GET; unset( $args['wcpay-link-handler'] ); $this->redirect_service->redirect_to_account_link( $args ); } } /** * Proxy method that's called in other classes that have access to account (not redirect_service) * to immediately redirect to the main "Welcome to WooPayments" onboarding page. * Note that this function immediately ends the execution. * * @param string|null $error_message Optional error message to show in a notice. */ public function redirect_to_onboarding_welcome_page( $error_message = null ) { $this->redirect_service->redirect_to_nox_flow(); } /** * Checks if everything is in working order and redirects to the connect page if not. * * @return bool True if the redirection happened. */ public function maybe_redirect_after_plugin_activation(): bool { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return false; } $is_on_settings_page = WC_Payments_Admin_Settings::is_current_page_settings(); $should_redirect_to_onboarding = (bool) get_option( 'wcpay_should_redirect_to_onboarding', false ); if ( // If not loading the settings page... ! $is_on_settings_page // ...and we have redirected before. && ! $should_redirect_to_onboarding ) { // Do not attempt to redirect again. return false; } if ( $should_redirect_to_onboarding ) { // Update the option. We try to redirect once and will not attempt to redirect again. update_option( 'wcpay_should_redirect_to_onboarding', false ); } // If everything is in working order, don't redirect. if ( $this->has_working_jetpack_connection() && $this->is_stripe_account_valid() ) { return false; } // Redirect to NOX onboarding flow. $this->redirect_service->redirect_to_nox_flow( WC_Payments_Onboarding_Service::FROM_PLUGIN_ACTIVATION, WC_Payments_Onboarding_Service::get_source() ); return true; } /** * Stores the account referral code and redirects to the connect page. * * @return void */ public function maybe_redirect_onboarding_referral(): void { if ( ! is_admin() || wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return; } if ( ! isset( $_GET['woopayments-ref'] ) ) { return; } // Return early and redirect to the overview page if already connected. if ( $this->is_stripe_account_valid() ) { $this->redirect_service->redirect_to_overview_page(); return; } $referral_code = sanitize_text_field( wp_unslash( $_GET['woopayments-ref'] ) ); $referral_code = $this->onboarding_service->normalize_and_store_referral_code( $referral_code ); // Return and redirect early if the code is invalid. if ( empty( $referral_code ) ) { $this->redirect_service->redirect_to_nox_flow(); return; } // Track the referral code. $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_REFERRAL, [ 'referral_code' => $referral_code, 'referrer' => wp_get_referer(), ] ); // Redirect to the NOX flow. $this->redirect_service->redirect_to_nox_flow( WC_Payments_Onboarding_Service::FROM_REFERRAL ); } /** * Redirects WooPayments settings to the connect page when there is no account or an invalid account. * * Every WooPayments page except connect are already hidden, but merchants can still access * it through WooCommerce settings. * * @return bool True if a redirection happened, false otherwise. */ public function maybe_redirect_from_settings_page(): bool { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return false; } $params = [ 'page' => 'wc-settings', 'tab' => 'checkout', 'section' => 'woocommerce_payments', ]; // We're not in the WooPayments settings page, don't redirect. if ( count( $params ) !== count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended return false; } // If everything is NOT in good working condition, redirect to the NOX flow. if ( ! $this->has_working_jetpack_connection() || ! $this->is_stripe_account_valid() ) { $this->redirect_service->redirect_to_nox_flow( WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_SETTINGS, WC_Payments_Onboarding_Service::SOURCE_WCADMIN_SETTINGS_PAGE ); return true; } // Everything is OK, don't redirect. return false; } /** * Redirects onboarding wizard page (payments/onboarding) to the overview page for accounts that have a valid Stripe account. * * Payments onboarding wizard page is already hidden for those who have a Stripe account connected, * but merchants can still access it by clicking back in the browser tab. * * @return bool True if the redirection happened, false otherwise. */ public function maybe_redirect_from_onboarding_wizard_page(): bool { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return false; } $params = [ 'page' => 'wc-admin', 'path' => '/payments/onboarding', ]; // We're not in the onboarding wizard page, don't redirect. if ( count( $params ) !== count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended return false; } // Determine the original source from where the merchant entered the onboarding flow. $onboarding_source = WC_Payments_Onboarding_Service::get_source(); // Prevent access to onboarding wizard if we don't have a working WPCOM/Jetpack connection. // Redirect back to the connect page with an error message. if ( ! $this->has_working_jetpack_connection() ) { $referer = sanitize_text_field( wp_get_raw_referer() ); // Track unsuccessful Jetpack connection. if ( strpos( $referer, 'wordpress.com' ) ) { $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_FAILURE, [ 'mode' => WC_Payments_Onboarding_Service::is_test_mode_enabled() ? 'test' : 'live', // Capture the user source of the connection attempt originating page. // This is the same source that is used to track the onboarding flow origin. 'source' => $onboarding_source, ] ); } // Redirect to the NOX flow. $this->redirect_service->redirect_to_nox_flow( WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD, $onboarding_source ); return true; } // We check it here after refreshing the cache, because merchant might have clicked back in browser (after Stripe KYC). // That will mean that no redirect from Stripe happened and user might be able to go through onboarding again if no webhook processed yet. // That might cause issues if user selects sandbox onboarding after live one. // Shouldn't be called with force disconnected option enabled, otherwise we'll get current account data. if ( ! WC_Payments_Utils::force_disconnected_enabled() ) { $this->refresh_account_data(); } // Don't redirect merchants that have no Stripe account connected. if ( ! $this->is_stripe_connected() ) { return false; } // Merchants with an invalid Stripe account, need to go to the Stripe KYC, not our onboarding wizard. if ( ! $this->is_stripe_account_valid() ) { // Redirect to the NOX flow to continue the Stripe KYC onboarding. $this->redirect_service->redirect_to_nox_flow( WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD, $onboarding_source ); return true; } $this->redirect_service->redirect_to_overview_page( WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD ); return true; } /** * Maybe redirects the connect page (payments/connect) * * We redirect to the overview page for stores that have a working Jetpack connection and a valid Stripe account. * * Note: Connect _page_ links are not the same as connect links. * Connect links are used to start/re-start/continue the onboarding flow and they are independent of * the WP dashboard page (based solely on request params). * * IMPORTANT: The logic should be kept in sync with the one in maybe_redirect_from_overview_page to avoid loops. * * @see self::maybe_redirect_from_overview_page() for the opposite redirection. * @see self::maybe_handle_onboarding() for connect links handling. * * @return bool True if the redirection happened, false otherwise. */ public function maybe_redirect_from_connect_page(): bool { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return false; } $params = [ 'page' => 'wc-admin', 'path' => '/payments/connect', ]; // We're not on the Connect page, don't redirect. if ( count( $params ) !== count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended return false; } // There are certain cases where it is best to refresh the account data // to be sure we are dealing with the current account state on the Connect page: // - When the merchant is coming from the onboarding wizard it is best to refresh the account data because // the merchant might have started the embedded Stripe KYC. // - When the merchant is coming from the embedded KYC, definitely refresh the account data. // The account data shouldn't be refreshed with force disconnected option enabled. if ( ! WC_Payments_Utils::force_disconnected_enabled() && in_array( WC_Payments_Onboarding_Service::get_from(), [ WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD, WC_Payments_Onboarding_Service::FROM_WCADMIN_NOX_IN_CONTEXT, WC_Payments_Onboarding_Service::FROM_ONBOARDING_KYC, ], true ) ) { $this->refresh_account_data(); } // If everything is in good working condition, redirect to Payments Overview page. if ( $this->has_working_jetpack_connection() && $this->is_stripe_account_valid() ) { $this->redirect_service->redirect_to_overview_page( WC_Payments_Onboarding_Service::FROM_CONNECT_PAGE ); return true; } // Determine from where the merchant was directed to the Connect page. $from = WC_Payments_Onboarding_Service::get_from(); // If the user came from the core Payments task list item or the WC Payments Settings NOX in-context flow, // skip the Connect page and go directly to the Jetpack connection flow and/or onboarding wizard. if ( in_array( $from, [ WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_TASK, WC_Payments_Onboarding_Service::FROM_WCADMIN_NOX_IN_CONTEXT, ], true ) ) { // We use a connect link to allow our logic to determine what comes next: // the Jetpack connection setup and/or onboarding wizard (MOX). $this->redirect_service->redirect_to_wcpay_connect( // The next step should treat the merchant as coming from the originating place, // not the Connect page. $from, [ 'source' => WC_Payments_Onboarding_Service::get_source(), ] ); return true; } return false; } /** * Redirects overview page (payments/overview) to the connect page for stores that * don't have a working Jetpack connection or a valid connected Stripe account. * * IMPORTANT: The logic should be kept in sync with the one in maybe_redirect_from_connect_page to avoid loops. * * @see self::maybe_redirect_from_connect_page() for the opposite redirection. * @see self::maybe_handle_onboarding() for connect links handling. * * @return bool True if the redirection happened, false otherwise. */ public function maybe_redirect_from_overview_page(): bool { if ( wp_doing_ajax() || ! current_user_can( 'manage_woocommerce' ) ) { return false; } $params = [ 'page' => 'wc-admin', 'path' => '/payments/overview', ]; // We're not on the Overview page, don't redirect. if ( count( $params ) !== count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended return false; } // If everything is NOT in good working condition, redirect to the NOX flow to complete connection. if ( ! $this->has_working_jetpack_connection() || ! $this->is_stripe_account_valid() ) { $this->redirect_service->redirect_to_nox_flow( WC_Payments_Onboarding_Service::FROM_OVERVIEW_PAGE, WC_Payments_Onboarding_Service::get_source() ); return true; } return false; } /** * Filter function to add Stripe to the list of allowed redirect hosts * * @param array $hosts Array of allowed hosts. * * @return array Filtered allowed hosts */ public function allowed_redirect_hosts( $hosts ) { $hosts[] = 'connect.stripe.com'; return $hosts; } /** * Handle onboarding (login/init/redirect) routes */ public function maybe_handle_onboarding() { if ( ! is_admin() || ! current_user_can( 'manage_woocommerce' ) ) { return; } // Determine what was the immediately previous step of the onboarding flow. $from = WC_Payments_Onboarding_Service::get_from(); // Determine the original/initial place from where the merchant entered the onboarding flow. $onboarding_source = WC_Payments_Onboarding_Service::get_source(); /** * ================== * Handle Stripe dashboard login links. * ================== */ if ( isset( $_GET['wcpay-login'] ) && check_admin_referer( 'wcpay-login' ) ) { try { if ( $this->is_stripe_connected() && ! $this->is_details_submitted() ) { $args = $_GET; $args['type'] = 'complete_kyc_link'; $this->redirect_service->redirect_to_account_link( $args ); } // Clear account cache when generating Stripe dashboard's login link. $this->clear_cache(); $this->redirect_service->redirect_to_login(); } catch ( Exception $e ) { Logger::error( 'Failed redirect_to_login: ' . $e->getMessage() ); $this->redirect_service->redirect_to_overview_page_with_error( [ 'wcpay-login-error' => '1' ] ); } // We should not reach this point as we either redirect to the Stripe dashboard // or to the Payments > Overview page with an error message. return; } /** * ================== * Handle the Jetpack re-connection in case of missing connection owner. * * @see self::get_wpcom_reconnect_url() * ================== */ if ( isset( $_GET['wcpay-reconnect-wpcom'] ) && check_admin_referer( 'wcpay-reconnect-wpcom' ) ) { // Track the Jetpack connection start. $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_START, [ 'is_reconnect' => true, 'from' => $from, ] ); $this->payments_api_client->start_server_connection( WC_Payments_Admin_Settings::get_settings_url() ); return; } /** * ================== * Handle connect links that trigger either the onboarding flow start or its continuation. * * "Onboarding flow" = the Jetpack connection + onboarding wizard + Stripe KYC. * * IMPORTANT: Connect links are NOT THE SAME as Connect page links: * - Connect links are used to start/re-start/continue the onboarding flow. * Connect links are WP admin links with the `wcpay-connect` GET parameter and a 'wcpay-connect' action nonce. * Connect links imply actions being taken on the merchant store and account setup (hence the need to be protected by a nonce). * - Connect page links are just the WC admin URL of the Connect page (page=wc-admin&path=/payments/connect). * * The BUSINESS LOGIC of handling connect links, in the order of priority: * * 0. Make changes to the account data if needed (e.g. reset account, disable test mode onboarding) * as instructed by the GET params. * 0.1 If we reset the account -> redirect to CONNECT PAGE / SETTINGS PAGE If redirect to settings page flag set * 1. Returning from the WPCOM/Jetpack connection screen. * 1.1 SUCCESSFUL connection * 1.1.1 NO Stripe account connected -> redirect to ONBOARDING WIZARD * 1.1.2 Stripe account connected -> redirect to OVERVIEW PAGE * 1.2 UNSUCCESSFUL connection -> redirect to CONNECT PAGE with ERROR message / SETTINGS PAGE if redirect to settings page flag set * 2. Working WPCOM/Jetpack connection and fully onboarded Stripe account -> redirect to OVERVIEW PAGE * 3. Specific `from` places -> redirect to CONNECT PAGE regardless of the account status * 4. NO [working] WPCOM/Jetpack connection: * Initialize the WPCOM registration and: * 4.1 On SUCCESS -> redirect to WPCOM/Jetpack connection screen (Calypso). * 4.2 On ERROR -> redirect to CONNECT PAGE with ERROR message * 5. Working WPCOM/Jetpack connection and: * 5.1 If NO Stripe account connected: * 5.1.1 If we are setting up a test drive account and the auto-start onboarding is enabled, * we redirect to the CONNECT PAGE to let the JS logic orchestrate the Stripe account creation. * 5.1.2 If we come from the ONBOARDING WIZARD: * Initialize the Stripe account and: * 5.1.1.1 On SUCCESS -> redirect to STRIPE KYC * 5.1.1.2 On existing account -> redirect to OVERVIEW PAGE * 5.1.1.3 On ERROR -> redirect to CONNECT PAGE with ERROR message * 5.1.3 All other cases -> redirect to ONBOARDING WIZARD * 5.2 If PARTIALLY onboarded Stripe account connected -> redirect to STRIPE KYC * 5.3 If fully onboarded Stripe account connected -> redirect to OVERVIEW PAGE * 5.3.1 If redirect to settings page flags set -> redirect to SETTINGS PAGE * * This logic is so complex because we use connect links as a catch-all place to * handle everything and anything related to the WooPayments account setup. It reduces the complexity on the * "outer-edges" of our ecosystem (e.g. Woo core, emails, etc.) and centralizes the handling in one place. * * IMPORTANT: Whenever we decide to change the business logic we should UPDATE THE COMMENT ABOVE!!! * Do NOT let this comment become stale/out-of-sync!!! * ================== */ if ( isset( $_GET['wcpay-connect'] ) && check_admin_referer( 'wcpay-connect' ) ) { $wcpay_connect_param = sanitize_text_field( wp_unslash( $_GET['wcpay-connect'] ) ); $incentive_id = ! empty( $_GET['promo'] ) ? sanitize_text_field( wp_unslash( $_GET['promo'] ) ) : ''; $collect_payout_requirements = ! empty( $_GET['collect_payout_requirements'] ) && 'true' === $_GET['collect_payout_requirements']; $create_test_drive_account = ! empty( $_GET['test_drive'] ) && 'true' === $_GET['test_drive']; $redirect_to_settings_page = ! empty( $_GET['redirect_to_settings_page'] ) && 'true' === $_GET['redirect_to_settings_page']; // There is no point in auto starting test drive onboarding if we are not in the test drive mode. $auto_start_test_drive_onboarding = $create_test_drive_account && ! empty( $_GET['auto_start_test_drive_onboarding'] ) && 'true' === $_GET['auto_start_test_drive_onboarding']; // We will onboard in test mode if the test_mode GET param is set, if we are creating a test drive account, // or if we are in dev mode. $should_onboard_in_test_mode = ( isset( $_GET['test_mode'] ) && wc_clean( wp_unslash( $_GET['test_mode'] ) ) ) || $create_test_drive_account || WC_Payments::mode()->is_dev(); // Hide menu notification badge upon starting setup. update_option( 'wcpay_menu_badge_hidden', 'yes' ); // By default, the next step will be informed that the user came from the Connect page. $next_step_from = WC_Payments_Onboarding_Service::FROM_CONNECT_PAGE; // Default props we want to attach to onboarding Tracks events. $tracks_props = [ 'incentive' => $incentive_id, 'mode' => $should_onboard_in_test_mode ? 'test' : 'live', 'from' => $from, 'source' => $onboarding_source, ]; // Handle the return from Stripe KYC flow (via a connect link). // The state of the WPCOM/Jetpack connection should not matter - if we received the state data, // we need to try and capture it. if ( isset( $_GET['wcpay-state'] ) && isset( $_GET['wcpay-mode'] ) ) { $state = sanitize_text_field( wp_unslash( $_GET['wcpay-state'] ) ); $mode = sanitize_text_field( wp_unslash( $_GET['wcpay-mode'] ) ); $this->finalize_connection( $state, $mode, [ 'from' => $from, 'source' => $onboarding_source, // Carry over some parameters as they may be used by our frontend logic. 'wcpay-sandbox-success' => ! empty( $_GET['wcpay-sandbox-success'] ) ? 'true' : false, 'test_drive_error' => ! empty( $_GET['test_drive_error'] ) ? 'true' : false, ] ); return; } // Remove the previously stored onboarding state if the merchant wants to start a new onboarding session // or if we came back from Stripe with an error but no state. // This will allow us to avoid errors when finalizing the account connection. if ( ( ! empty( $_GET['wcpay-discard-started-onboarding'] ) && 'true' === $_GET['wcpay-discard-started-onboarding'] ) || ( WC_Payments_Onboarding_Service::FROM_STRIPE === $from && ! empty( $_GET['wcpay-connection-error'] ) ) ) { delete_transient( self::ONBOARDING_STATE_TRANSIENT ); delete_option( self::EMBEDDED_KYC_IN_PROGRESS_OPTION ); } // Make changes to account data as instructed by action GET params. // This needs to happen early because we need to make things "not OK" for the rest of the logic. if ( ! empty( $_GET['wcpay-reset-account'] ) && 'true' === $_GET['wcpay-reset-account'] ) { // If the account does not exist, there's nothing to reset. Redirect the merchant to the NOX flow where the error will be shown. if ( ! $this->is_stripe_connected() ) { $this->redirect_service->redirect_to_nox_flow( $from, $onboarding_source ); return; } $test_mode_onboarding = WC_Payments_Onboarding_Service::is_test_mode_enabled(); try { // Immediately change the account cache to avoid API requests during the time it takes for // the Transact Platform to actually delete the account. $this->overwrite_cache_with_no_account(); // Delete the currently Stripe connected account, in the onboarding mode we are currently in. $this->payments_api_client->delete_account( $test_mode_onboarding ); } catch ( API_Exception $e ) { // In case we fail to delete the account, log, force refresh the account cache // and redirect to the Overview page. Logger::error( 'Failed to delete account: ' . $e->getMessage() ); $this->refresh_account_data(); $this->redirect_service->redirect_to_overview_page_with_error( [ 'wcpay-reset-account-error' => '1' ] ); return; } $this->onboarding_service->cleanup_on_account_reset(); delete_transient( self::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT ); // Delete the NOX profile option on account reset. // This is needed to ensure merchants are not stuck with account reset and profile option set. delete_option( self::NOX_PROFILE_OPTION_KEY ); // Track the onboarding (not account) reset. $this->tracks_event( WC_Payments_Onboarding_Service::TRACKS_EVENT_ONBOARDING_RESET, array_merge( $tracks_props, [ 'mode' => $test_mode_onboarding ? 'test' : 'live' ] ) ); // When we reset the account and want to go back to the settings page - redirect immediately! if ( $redirect_to_settings_page ) { $this->redirect_service->redirect_to_settings_page( WC_Payments_Onboarding_Service::FROM_RESET_ACCOUNT, [ 'source' => $onboarding_source ] ); return; } // Otherwise, when we reset the account we want to always go the Connect page. Redirect immediately! $this->redirect_service->redirect_to_nox_flow( WC_Payments_Onboarding_Service::FROM_RESET_ACCOUNT, $onboarding_source ); return; } elseif ( ! empty( $_GET['wcpay-disable-onboarding-test-mode'] ) && 'true' === $_GET['wcpay-disable-onboarding-test-mode'] ) { // If the account does not exist, redirect the merchant to the NOX flow where the error will be shown. if ( ! $this->is_stripe_connected() ) { $this->redirect_service->redirect_to_nox_flow( $from, $onboarding_source ); return; } // If the test mode onboarding is enabled: // - Delete the current account; // - Cleanup the gateway state for a fresh onboarding flow. // Otherwise, we are already using a live account and the request is invalid (it will be handled below, // in the "everything OK" scenario). if ( WC_Payments_Onboarding_Service::is_test_mode_enabled() ) { try { // If we're in test mode and dealing with a test-drive account, // we need to collect the test drive settings before we delete the test-drive account, // and apply those settings to the live account. $this->save_test_drive_settings(); // Immediately change the account cache to avoid API requests during the time it takes for // the Transact Platform to actually delete the account. $this->overwrite_cache_with_no_account(); // Delete the currently connected Stripe account. $this->payments_api_client->delete_account( true ); } catch ( API_Exception $e ) { // In case we fail to delete the account, log and carry on. Logger::error( 'Failed to delete account in test mode: ' . $e->getMessage() ); } $this->onboarding_service->cleanup_on_account_reset(); } // Since we are moving from test to live, we will only onboard in test mode if we are in dev mode. // Otherwise, we will do a live onboarding. $should_onboard_in_test_mode = WC_Payments::mode()->is_dev(); $next_step_from = WC_Payments_Onboarding_Service::FROM_TEST_TO_LIVE; // These from values are allowed to be passed through, when going from test to live. if ( in_array( $from, [ WC_Payments_Onboarding_Service::FROM_SETTINGS, WC_Payments_Onboarding_Service::FROM_OVERVIEW_PAGE, WC_Payments_Onboarding_Service::FROM_GO_LIVE_TASK, ], true ) ) { $next_step_from = $from; } } // Handle the return from the WPCOM/Jetpack connection screens. // The merchant either completed the connection or failed. We handle both scenarios. // Note: this should be handled early since the Jetpack connection is the first requirement // in our onboarding stack. if ( isset( $_GET['wcpay-connect-jetpack-success'] ) ) { // If the merchant failed to set up the WPCOM/Jetpack connection, // we redirect them back to the Connect page with an error message. if ( ! $this->has_working_jetpack_connection() ) { // Track unsuccessful Jetpack connection. $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_FAILURE, // Use the DB stored onboarding mode, not the one determined from the current request params. array_merge( $tracks_props, [ 'mode' => WC_Payments_Onboarding_Service::is_test_mode_enabled() ? 'test' : 'live' ] ) ); if ( $redirect_to_settings_page ) { $this->redirect_service->redirect_to_settings_page( WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION, [ 'source' => $onboarding_source, 'wcpay-connect-jetpack-error' => '1', ] ); } $this->redirect_service->redirect_to_connect_page( sprintf( /* translators: %s: WooPayments */ __( 'Connection to WordPress.com failed. Please connect to WordPress.com to start using %s.', 'woocommerce-payments' ), 'WooPayments' ), WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION, [ 'source' => $onboarding_source, ] ); return; } // Track successful Jetpack connection. $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_SUCCESS, $tracks_props ); // Always clear the account cache after establishing the Jetpack/WPCOM connection. // An account may already be available on our platform for this store. $this->clear_cache(); } // Handle the "everything OK" scenario. // Handle this _after_ we've handled the actions that make things "not OK" (e.g. resetting account or // moving from test to live). // Payout requirement collection needs to bypass the "everything OK" scenario. if ( ! $collect_payout_requirements && $this->has_working_jetpack_connection() && $this->is_stripe_account_valid() ) { $params = [ 'source' => $onboarding_source, // Carry over some parameters as they may be used by our frontend logic. 'wcpay-connection-success' => ! empty( $_GET['wcpay-connection-success'] ) ? '1' : false, 'wcpay-sandbox-success' => ! empty( $_GET['wcpay-sandbox-success'] ) ? 'true' : false, 'test_drive_error' => ! empty( $_GET['test_drive_error'] ) ? 'true' : false, ]; if ( $redirect_to_settings_page ) { $this->redirect_service->redirect_to_settings_page( $from, $params ); return; } $this->redirect_service->redirect_to_overview_page( $from, $params ); return; } // Handle the specific from places that need to go to the NOX flow and start onboarding from there. if ( in_array( $from, [ WC_Payments_Onboarding_Service::FROM_STRIPE, ], true ) // This is a weird case, but it is best to handle it. || ( WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD === $from && ! $this->has_working_jetpack_connection() ) // Redirect merchants coming from settings page to the connect page only if $redirect_to_settings_page is false. || ( WC_Payments_Onboarding_Service::FROM_WCADMIN_PAYMENTS_SETTINGS === $from && ! $redirect_to_settings_page ) ) { $this->redirect_service->redirect_to_connect_page( ! empty( $_GET['wcpay-connection-error'] ) ? sprintf( /* translators: 1: WooPayments. */ __( 'Please <b>complete your %1$s setup</b> to process transactions.', 'woocommerce-payments' ), 'WooPayments' ) : null, null, // Do not carry over the `from` value to avoid redirect loops. [ 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, 'collect_payout_requirements' => $collect_payout_requirements ? 'true' : false, 'source' => $onboarding_source, ] ); return; } // Track WooPayments onboarding (aka account connection) start. // We should not have a connected Stripe account. If we do, it means we are not at the very start, // but somewhere in between. // Exclude returns from the WPCOM/Jetpack connection. // This needs to happen _before_ we attempt to init the WPCOM/Jetpack connection! if ( ! isset( $_GET['wcpay-connect-jetpack-success'] ) && ! $this->is_stripe_connected() ) { $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_START, $tracks_props ); } // First requirement: handle the WPCOM/Jetpack connection. // If there is a working one, we can proceed with the Stripe account handling. try { $this->maybe_init_jetpack_connection( // Carry over all the important GET params, so we have them after the Jetpack connection setup. add_query_arg( [ 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, 'collect_payout_requirements' => $collect_payout_requirements ? 'true' : false, 'test_mode' => $should_onboard_in_test_mode ? 'true' : false, 'test_drive' => $create_test_drive_account ? 'true' : false, 'auto_start_test_drive_onboarding' => $auto_start_test_drive_onboarding ? 'true' : false, // These are starting capabilities for the account. // They are collected by the payment method step of the // WC Payments settings page native onboarding experience. 'capabilities' => rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ), 'from' => WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION, 'source' => $onboarding_source, 'redirect_to_settings_page' => $redirect_to_settings_page ? 'true' : false, ], self::get_connect_url( $wcpay_connect_param ) // Instruct Jetpack to return here (connect link). ), $tracks_props ); } catch ( API_Exception $e ) { Logger::error( 'Init Jetpack connection failed. ' . $e->getMessage() ); $this->redirect_service->redirect_to_connect_page( /* translators: %s: error message. */ sprintf( __( 'There was a problem connecting your store to WordPress.com: "%s"', 'woocommerce-payments' ), $e->getMessage() ), WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION, [ 'source' => $onboarding_source, ] ); return; } // Handle the scenarios that need to point to the onboarding wizard before initializing the Stripe onboarding. // All other more specific scenarios should have been handled by this point. if ( ! $create_test_drive_account // When we come from the onboarding wizard we obviously don't want to go back to it! && WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD !== $from && ! $this->is_stripe_connected() ) { $additional_params = [ 'source' => $onboarding_source, ]; if ( $this->onboarding_service->get_capabilities_from_request() ) { $additional_params['capabilities'] = rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ); } $this->redirect_service->redirect_to_onboarding_wizard( // When we redirect to the onboarding wizard, we carry over the `from`, if we have it. // This is because there is no interim step between the user clicking the connect link and the onboarding wizard. ! empty( $from ) ? $from : $next_step_from, $additional_params ); return; } // Handle the Stripe account initialization and/or redirect to the Stripe KYC. // This is used at the end of our onboarding wizard (MOX) and whenever the merchant needs to // finish Stripe KYC verifications (like in the case of partially onboarded accounts). // In case everything is already OK and there is no need for Stripe KYC, // the merchant will get redirected to the Payments > Overview page. try { // Prevent duplicate requests to start the onboarding flow. if ( $this->onboarding_service->is_onboarding_init_in_progress() ) { Logger::warning( 'Duplicate onboarding attempt detected.' ); $this->redirect_service->redirect_to_connect_page( __( 'There was a duplicate attempt to initiate account setup. Please wait a few seconds and try again.', 'woocommerce-payments' ) ); return; } // If we are creating a test-drive account, we do things a little different. if ( $create_test_drive_account ) { // Since there should be no Stripe KYC needed, make sure we start with a clean state. delete_transient( self::ONBOARDING_STATE_TRANSIENT ); delete_option( self::EMBEDDED_KYC_IN_PROGRESS_OPTION ); // If we have the auto_start_test_drive_onboarding flag, we redirect to the Connect page // to let the JS logic take control and orchestrate things. if ( $auto_start_test_drive_onboarding ) { $this->redirect_service->redirect_to_connect_page( null, $from, // Carry over `from` since we are doing a short-circuit. [ 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, 'test_drive' => 'true', 'auto_start_test_drive_onboarding' => 'true', // This is critical. // These are starting capabilities for the account. // They are collected by the payment method step of the // WC Payments settings page native onboarding experience. 'capabilities' => rawurlencode( wp_json_encode( $this->onboarding_service->get_capabilities_from_request() ) ), 'test_mode' => $should_onboard_in_test_mode ? 'true' : false, 'source' => $onboarding_source, 'redirect_to_settings_page' => $redirect_to_settings_page ? 'true' : false, ] ); return; } } // Check if there is already an onboarding flow started. if ( get_transient( self::ONBOARDING_STATE_TRANSIENT ) ) { // Carry over all relevant GET params to the confirmation URL. // We don't need to carry over reset account or test_to_live params because those actions // automatically discard any ongoing onboarding. // Also, do not carry over auto_start_test_drive_onboarding as we want the merchant to see the notice. $confirmation_url = add_query_arg( [ 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, 'collect_payout_requirements' => $collect_payout_requirements ? 'true' : false, 'test_drive' => $create_test_drive_account ? 'true' : false, 'test_mode' => ( ! empty( $_GET['test_mode'] ) && wc_clean( wp_unslash( $_GET['test_mode'] ) ) ) ? 'true' : false, 'from' => $from, // Use the same from. 'source' => $onboarding_source, 'wcpay-discard-started-onboarding' => 'true', ], self::get_connect_url( $wcpay_connect_param ) // Instruct Jetpack to return here (connect link). ); $this->redirect_service->redirect_to_connect_page( sprintf( /* translators: 1: anchor opening markup 2: closing anchor markup */ __( 'Another account setup session is already in progress. Please finish it or %1$sclick here to start again%2$s.', 'woocommerce-payments' ), '<a href="' . esc_url( $confirmation_url ) . '">', '</a>' ) ); return; } // Mark the onboarding initialization as in progress. $this->onboarding_service->set_onboarding_init_in_progress(); $redirect_to = $this->init_stripe_onboarding( $create_test_drive_account ? 'test_drive' : ( $should_onboard_in_test_mode ? 'test' : 'live' ), $wcpay_connect_param, [ 'promo' => ! empty( $incentive_id ) ? $incentive_id : false, 'collect_payout_requirements' => $collect_payout_requirements ? 'true' : false, 'source' => $onboarding_source, 'from' => WC_Payments_Onboarding_Service::FROM_STRIPE, ] ); $this->onboarding_service->clear_onboarding_init_in_progress(); // Always clear the account cache after a Stripe onboarding init attempt. // This allows the merchant to use connect links to refresh its account cache, in case something is wrong. $this->clear_cache(); // Make sure the redirect URL is safe. $redirect_to = wp_sanitize_redirect( $redirect_to ); $redirect_to = wp_validate_redirect( $redirect_to ); // When creating test-drive accounts, // reply with a JSON so the JS logic can pick it up and redirect the merchant. if ( $create_test_drive_account && ! empty( $redirect_to ) ) { wp_send_json_success( [ 'redirect_to' => $redirect_to ] ); } else { // Redirect the user to where our Stripe onboarding instructed (or to our own embedded Stripe KYC). $this->redirect_service->redirect_to( $redirect_to ); } } catch ( API_Exception $e ) { $this->onboarding_service->clear_onboarding_init_in_progress(); // Always clear the account cache in case of errors. $this->clear_cache(); Logger::error( 'Init Stripe onboarding failed. ' . $e->getMessage() ); $this->redirect_service->redirect_to_connect_page( sprintf( /* translators: %s: WooPayments. */ __( 'There was a problem setting up your %s account. Please try again.', 'woocommerce-payments' ), 'WooPayments' ), null, [ 'source' => $onboarding_source, ] ); return; } // Stop here when running unit tests. if ( defined( 'WCPAY_TEST_ENV' ) && WCPAY_TEST_ENV ) { return; } // We should not reach this point as the merchant should be redirected to the proper place already. // But, as a failsafe, redirect to either the Payments > Overview page or the Connect page. Logger::warning( 'Doing the failsafe WooPayments connect link redirect.' ); if ( $this->is_stripe_connected() && $this->has_working_jetpack_connection() ) { $this->redirect_service->redirect_to_overview_page(); } else { $this->redirect_service->redirect_to_connect_page( null, null, [ 'source' => $onboarding_source ] ); } return; } /** * ================== * Handle the redirect back from the Stripe KYC (proxied through our platform) * when it didn't come through a connect link. * * @see self::finalize_connection() * ================== */ if ( isset( $_GET['wcpay-state'] ) && isset( $_GET['wcpay-mode'] ) ) { $state = sanitize_text_field( wp_unslash( $_GET['wcpay-state'] ) ); $mode = sanitize_text_field( wp_unslash( $_GET['wcpay-mode'] ) ); $this->finalize_connection( $state, $mode, [ 'from' => $from, 'source' => $onboarding_source, ] ); return; } } /** * Get Stripe login url. * * @return string Stripe account login url. */ private function get_login_url() { return add_query_arg( // nosemgrep: audit.php.wp.security.xss.query-arg -- no user input data used. [ 'wcpay-login' => '1', '_wpnonce' => wp_create_nonce( 'wcpay-login' ), ] ); } /** * Get provider onboarding page url. * * @return string */ public function get_provider_onboarding_page_url(): string { return add_query_arg( [ 'page' => 'wc-settings', 'tab' => 'checkout', 'path' => '/woopayments/onboarding', ], admin_url( 'admin.php' ) ); } /** * Get connect url. * * @param string $wcpay_connect_from Optional. A value to inform the connect logic where the user came from. * It will allow us to adapt behavior and/or UX (e.g. redirect to different places). * * @return string Connect URL. */ public static function get_connect_url( $wcpay_connect_from = '1' ) { // The minimal params that make a connect URL. $url_params = [ 'wcpay-connect' => $wcpay_connect_from, '_wpnonce' => wp_create_nonce( 'wcpay-connect' ), ]; // Attach our best guess of the onboarding source. $url_params['source'] = WC_Payments_Onboarding_Service::get_source(); return add_query_arg( $url_params, admin_url( 'admin.php' ) ); } /** * Payments task page url * * @deprecated 7.8.0 * * @return string payments task page url */ public static function get_payments_task_page_url() { wc_deprecated_function( __FUNCTION__, '7.8.0' ); return add_query_arg( [ 'page' => 'wc-admin', 'task' => 'payments', 'method' => 'wcpay', ], admin_url( 'admin.php' ) ); } /** * Get overview page url * * @return string overview page url */ public static function get_overview_page_url() { return add_query_arg( [ 'page' => 'wc-admin', 'path' => '/payments/overview', ], admin_url( 'admin.php' ) ); } /** * Checks if the current page is overview page * * @return boolean */ public static function is_overview_page() { return isset( $_GET['path'] ) && '/payments/overview' === $_GET['path']; } /** * Get WPCOM/Jetpack reconnect url, for use in case of missing connection owner. * * @return string WPCOM/Jetpack reconnect url. */ public static function get_wpcom_reconnect_url() { return admin_url( add_query_arg( [ 'wcpay-reconnect-wpcom' => '1', '_wpnonce' => wp_create_nonce( 'wcpay-reconnect-wpcom' ), ], 'admin.php' ) ); } /** * Has on-boarding been disabled? * * @return boolean */ public static function is_on_boarding_disabled() { // If the transient isn't set at all, we'll get false indicating that the server hasn't informed us that // on-boarding has been disabled (i.e. it's enabled as far as we know). return get_transient( self::ONBOARDING_DISABLED_TRANSIENT ); } /** * Starts the Jetpack connection flow if it's not already fully connected. * * @param string $return_url Where to redirect the user back to. * @param array $tracks_props Additional properties to attach to the Tracks event. * * @throws API_Exception If there was an error when registering the site on WP.com. */ private function maybe_init_jetpack_connection( string $return_url, array $tracks_props ) { // Nothing to do if we already have a working Jetpack connection. if ( $this->has_working_jetpack_connection() ) { return; } // Track the Jetpack connection start. $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_WPCOM_CONNECTION_START, $tracks_props ); // Ensure our success param is present. $return_url = add_query_arg( [ 'wcpay-connect-jetpack-success' => '1' ], $return_url ); $this->payments_api_client->start_server_connection( $return_url ); } /** * Builds the URL to return the user to after the Jetpack/Onboarding flow. * * @param string $wcpay_connect_from - Constant to decide where the user should be returned to after connecting. * * @return string */ private function get_onboarding_return_url( string $wcpay_connect_from ): string { $is_from_subscription_product_publish = preg_match( '/WC_SUBSCRIPTIONS_PUBLISH_PRODUCT_(\d+)/', $wcpay_connect_from, $matches ); if ( 1 === $is_from_subscription_product_publish ) { return add_query_arg( // nosemgrep: audit.php.wp.security.xss.query-arg -- specific admin url passed in. [ 'wcpay-subscriptions-onboarded' => '1' ], get_edit_post_link( $matches[1], 'url' ) ); } // Custom return URL for the connect page based on the source. // Defaults to using a connect link - this way we will route the user to the correct place. switch ( $wcpay_connect_from ) { case 'WC_SUBSCRIPTIONS_TABLE': return admin_url( add_query_arg( [ 'post_type' => 'shop_subscription' ], 'edit.php' ) ); case WC_Payments_Onboarding_Service::FROM_WCADMIN_NOX_IN_CONTEXT: // Build the URL to point to the WC NOX in-context onboarding. $params = [ 'page' => 'wc-admin', 'tab' => 'checkout', 'path' => '/woopayments/onboarding', ]; return admin_url( add_query_arg( $params, 'admin.php' ) ); default: return static::get_connect_url(); } } /** * Get the URL to the embedded onboarding KYC page. * * @param array $additional_args Additional query args to add to the URL. * * @return string */ private function get_onboarding_kyc_url( array $additional_args = [] ): string { $params = [ 'page' => 'wc-admin', 'path' => '/payments/onboarding/kyc', ]; $params = array_merge( $params, $additional_args ); return admin_url( add_query_arg( $params, 'admin.php' ) ); } /** * Initializes the onboarding flow by fetching the URL from the API and redirecting to it. * * @param string $setup_mode The onboarding setup mode. It should only be `live`, `test`, or `test_drive`. * On invalid value, it will default to `live`. * @param string $wcpay_connect_from Where the user should be returned to after connecting. * @param array $additional_args Additional query args to add to the return URL. * * @return string The URL to redirect the user to. Empty string if there is no URL to redirect to. * @throws API_Exception */ private function init_stripe_onboarding( string $setup_mode, string $wcpay_connect_from, array $additional_args = [] ): string { if ( ! in_array( $setup_mode, [ 'live', 'test', 'test_drive' ], true ) ) { $setup_mode = 'live'; } // Flag to collect payout requirements. $collect_payout_requirements = ! empty( $_GET['collect_payout_requirements'] ) && 'true' === $_GET['collect_payout_requirements']; // Make sure the onboarding test mode DB flag is set. WC_Payments_Onboarding_Service::set_test_mode( 'live' !== $setup_mode ); if ( ! $collect_payout_requirements ) { // Clear onboarding related account options if this is an initial onboarding attempt. WC_Payments_Onboarding_Service::clear_account_options(); } /* * If we are in the middle of an embedded onboarding, or this is an attempt to finalize PO, go to the KYC page. * In this case, we don't need to generate a return URL from Stripe, and we can rely on the JS logic to generate the session. */ if ( $this->onboarding_service->is_embedded_kyc_in_progress() || $collect_payout_requirements ) { // We want to carry over the connect link from value because with embedded KYC // there is no interim step for the user. $additional_args['from'] = WC_Payments_Onboarding_Service::get_from(); return $this->get_onboarding_kyc_url( $additional_args ); } // Else, go on with the normal onboarding redirect logic. $return_url = $this->get_onboarding_return_url( $wcpay_connect_from ); if ( ! empty( $additional_args ) ) { $return_url = add_query_arg( $additional_args, $return_url ); } $self_assessment_data = isset( $_GET['self_assessment'] ) ? wc_clean( wp_unslash( $_GET['self_assessment'] ) ) : []; if ( 'test_drive' === $setup_mode ) { // If we get to the overview page, we want to show the success message. $return_url = add_query_arg( 'wcpay-sandbox-success', 'true', $return_url ); } elseif ( 'test' === $setup_mode ) { // If we get to the overview page, we want to show the success message. $return_url = add_query_arg( 'wcpay-sandbox-success', 'true', $return_url ); } $site_data = [ 'site_username' => wp_get_current_user()->user_login, 'site_locale' => get_locale(), ]; $user_data = $this->onboarding_service->get_onboarding_user_data(); $account_data = $this->onboarding_service->get_account_data( $setup_mode, $self_assessment_data, // These are starting capabilities for the account. // They are collected by the payment method step of the // WC Payments settings page native onboarding experience. $this->onboarding_service->get_capabilities_from_request() ); $onboarding_data = $this->payments_api_client->get_onboarding_data( 'live' === $setup_mode, $return_url, $site_data, WC_Payments_Utils::array_filter_recursive( $user_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. WC_Payments_Utils::array_filter_recursive( $account_data ), // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- output of array_filter is escaped. WC_Payments_Onboarding_Service::get_actioned_notes(), $collect_payout_requirements, $this->onboarding_service->get_referral_code() ); // Check if we should enable WooPay by default respecing the WooPay value from capabilities request list. $should_enable_woopay = $this->onboarding_service->should_enable_woopay( filter_var( $onboarding_data['woopay_enabled_by_default'] ?? false, FILTER_VALIDATE_BOOLEAN ), $this->onboarding_service->get_capabilities_from_request() ); $is_test_mode = in_array( $setup_mode, [ 'test', 'test_drive' ], true ); $account_already_exists = isset( $onboarding_data['url'] ) && false === $onboarding_data['url']; // Only store the 'woopay_enabled_by_default' flag in a transient, to be enabled later, if // it should be enabled and the account doesn't already exist, or we are in test mode. if ( $should_enable_woopay && ( ! $account_already_exists || $is_test_mode ) ) { set_transient( self::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT, $should_enable_woopay, DAY_IN_SECONDS ); } // If an account already exists for this site and/or there is no need for KYC verifications, we're done. // Our platform will respond with a `false` URL in this case. if ( $account_already_exists ) { // Set the gateway options. $gateway = WC_Payments::get_gateway(); $gateway->update_option( 'enabled', 'yes' ); $gateway->update_option( 'test_mode', empty( $onboarding_data['is_live'] ) ? 'yes' : 'no' ); /** * ================== * Enforces the update of payment methods to 'enabled' based on the capabilities * provided during the NOX onboarding process. * * @see WC_Payments_Onboarding_Service::update_enabled_payment_methods_ids * ================== */ $capabilities = $this->onboarding_service->get_capabilities_from_request(); // Activate enabled Payment Methods IDs. if ( ! empty( $capabilities ) ) { $this->onboarding_service->update_enabled_payment_methods_ids( $gateway, $capabilities ); } // Store a state after completing KYC for tracks. This is stored temporarily in option because // user might not have agreed to TOS yet. update_option( '_wcpay_onboarding_stripe_connected', [ 'is_existing_stripe_account' => true ] ); // Clean up any existing onboarding state. delete_transient( self::ONBOARDING_STATE_TRANSIENT ); // Clear the embedded KYC in progress option, since the onboarding flow is now complete. $this->onboarding_service->clear_embedded_kyc_in_progress(); return add_query_arg( [ 'wcpay-connection-success' => '1' ], $return_url ); } // Save the onboarding state for a day. // This is used to verify the state when finalizing the onboarding and connecting the account. // On finalizing the onboarding, the transient gets deleted. set_transient( self::ONBOARDING_STATE_TRANSIENT, $onboarding_data['state'] ?? '', DAY_IN_SECONDS ); return (string) ( $onboarding_data['url'] ?? '' ); } /** * Activates WooPay when visiting the KYC success page and woopay_enabled_by_default transient is set to true. */ public function maybe_activate_woopay() { if ( ! isset( $_GET['wcpay-connection-success'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return; } if ( get_transient( self::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT ) ) { WC_Payments::get_gateway()->update_is_woopay_enabled( true ); delete_transient( self::WOOPAY_ENABLED_BY_DEFAULT_TRANSIENT ); } } /** * Handle the finalization of an embedded onboarding. This includes updating the cache, setting the gateway mode, * tracking the event, and redirecting the user to the overview page. * * @param string $mode The mode in which the account was created. Either 'test' or 'live'. * @param array $additional_args Additional query args to add to the redirect URLs. * * @return array Returns whether the operation was successful, along with the URL params to handle the redirect. */ public function finalize_embedded_connection( string $mode, array $additional_args = [] ): array { // Clear the account cache. $this->clear_cache(); // Set the gateway options. $gateway = WC_Payments::get_gateway(); $gateway->update_option( 'enabled', 'yes' ); $gateway->update_option( 'test_mode', 'live' !== $mode ? 'yes' : 'no' ); // Store a state after completing KYC for tracks. This is stored temporarily in option because // user might not have agreed to TOS yet. update_option( '_wcpay_onboarding_stripe_connected', [ 'is_existing_stripe_account' => false ] ); // Track account connection finish. $event_properties = [ 'mode' => 'test' === $mode ? 'test' : 'live', 'incentive' => ! empty( $additional_args['promo'] ) ? sanitize_text_field( $additional_args['promo'] ) : '', 'from' => ! empty( $additional_args['from'] ) ? sanitize_text_field( $additional_args['from'] ) : '', 'source' => ! empty( $additional_args['source'] ) ? sanitize_text_field( $additional_args['source'] ) : '', ]; $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_FINISHED, $event_properties ); // Clean up data used only during the onboarding process. $this->onboarding_service->cleanup_on_account_onboarded(); $params = $additional_args; $params['wcpay-connection-success'] = '1'; return [ 'success' => true, 'params' => $params, ]; } /** * Once the API redirects back to the site after the onboarding flow, verifies the parameters and stores the data. * * @param string $state Secret string. * @param string $mode Mode in which this account has been created. Either 'test' or 'live'. * @param array $additional_args Additional query args to add to redirect URLs. */ private function finalize_connection( string $state, string $mode, array $additional_args = [] ) { // If the state is not the same as the one we stored, something went wrong. // Reject the connection and redirect to the Connect page for another try (this doesn't mean the merchant will // need to set up a new account, just that it will need to do another round trip of server requests, // with a new secret, etc.). if ( get_transient( self::ONBOARDING_STATE_TRANSIENT ) !== $state ) { $this->redirect_service->redirect_to_connect_page( __( 'There was a problem processing your account data. Please try again.', 'woocommerce-payments' ), null, // No need to specify any from as we will carry over the once in the additional args, if present. $additional_args ); return; } // The states match, so we can delete the stored one. delete_transient( self::ONBOARDING_STATE_TRANSIENT ); // Clear the account cache. $this->clear_cache(); // Set the gateway options. $gateway = WC_Payments::get_gateway(); $gateway->update_option( 'enabled', 'yes' ); $gateway->update_option( 'test_mode', 'live' !== $mode ? 'yes' : 'no' ); /** * ================== * Enforces the update of payment methods to 'enabled' based on the capabilities * provided during the NOX onboarding process. * * @see WC_Payments_Onboarding_Service::update_enabled_payment_methods_ids * ================== */ $capabilities = $this->onboarding_service->get_capabilities_from_request(); // Activate enabled Payment Methods IDs. if ( ! empty( $capabilities ) ) { $this->onboarding_service->update_enabled_payment_methods_ids( $gateway, $capabilities ); } // Store a state after completing KYC for tracks. This is stored temporarily in option because // user might not have agreed to TOS yet. update_option( '_wcpay_onboarding_stripe_connected', [ 'is_existing_stripe_account' => false ] ); // Track account connection finish. $incentive_id = ! empty( $_GET['promo'] ) ? sanitize_text_field( wp_unslash( $_GET['promo'] ) ) : ''; $tracks_props = [ 'incentive' => $incentive_id, 'mode' => 'live' !== $mode ? 'test' : 'live', 'from' => $additional_args['from'] ?? '', 'source' => $additional_args['source'] ?? '', ]; $this->tracks_event( self::TRACKS_EVENT_ACCOUNT_CONNECT_FINISHED, $tracks_props ); $params = $additional_args; if ( ! empty( $_GET['wcpay-connection-error'] ) ) { // If we get this parameter, but we have a valid state, it means the merchant left KYC early and didn't finish it. // While we do have an account, it is not yet valid. We need to redirect them back to the connect page. $params['wcpay-connection-error'] = '1'; $this->redirect_service->redirect_to_connect_page( '', WC_Payments_Onboarding_Service::FROM_STRIPE, $params ); return; } // Clean up data used only during the onboarding process. $this->onboarding_service->cleanup_on_account_onboarded(); $params['wcpay-connection-success'] = '1'; $this->redirect_service->redirect_to_overview_page( WC_Payments_Onboarding_Service::FROM_STRIPE, $params ); } /** * Gets and caches the data for the account connected to this site. * * @param bool $force_refresh Forces data to be fetched from the server, rather than using the cache. * * @return array|bool Account data or false if failed to retrieve account data. */ public function get_cached_account_data( bool $force_refresh = false ) { if ( ! $this->payments_api_client->is_server_connected() ) { return []; } $refreshed = false; $account = $this->database_cache->get_or_add( Database_Cache::ACCOUNT_KEY, function () { try { // Since we're about to call the server again, clear out the onboarding disabled flag. // We can let the code below re-create it if the server tells us onboarding is still disabled. delete_transient( self::ONBOARDING_DISABLED_TRANSIENT ); $request = Get_Account::create(); $response = $request->send(); $account = $response->to_array(); } catch ( API_Exception $e ) { if ( 'wcpay_account_not_found' === $e->get_error_code() ) { // Special case - detect account not connected and cache it. $account = []; } elseif ( 'wcpay_on_boarding_disabled' === $e->get_error_code() ) { // Special case - detect account not connected and onboarding disabled. This will get updated the // next time we call the server for account information, but just in case we set the expiry time for // this setting an hour longer than the account details transient. $account = []; set_transient( self::ONBOARDING_DISABLED_TRANSIENT, true, 2 * HOUR_IN_SECONDS ); } else { // Return false to signal account retrieval error. return false; } } if ( ! $this->is_valid_cached_account( $account ) ) { return false; } return $account; }, [ $this, 'is_valid_cached_account' ], $force_refresh, $refreshed ); if ( null === $account ) { return false; } if ( $refreshed ) { /** * Allow us to tie in functionality to an account refresh. * * @param array|bool $account Account data or false if failed to retrieve account data. * * @since 4.3.0 */ do_action( 'woocommerce_payments_account_refreshed', $account ); } return $account; } /** * Updates the cached account data. * * @param string $property Property to update. * @param mixed $data Data to update. * * @return void */ public function update_account_data( $property, $data ) { $account_data = $this->database_cache->get( Database_Cache::ACCOUNT_KEY, true ); if ( ! is_array( $account_data ) ) { // Bail if we don't have any cached account data. return; } $account_data[ $property ] = is_array( $data ) ? array_merge( $account_data[ $property ] ?? [], $data ) : $data; $this->database_cache->add( Database_Cache::ACCOUNT_KEY, $account_data ); } /** * Refetches account data and returns the fresh data. * * @return array|bool Either the new account data or false if unavailable. */ public function refresh_account_data() { return $this->get_cached_account_data( true ); } /** * Change the account cache to hold the connected-but-no-account value (empty array). * * @return void */ public function overwrite_cache_with_no_account(): void { $this->database_cache->add( Database_Cache::ACCOUNT_KEY, [] ); } /** * Checks if the cached account can be used in the current plugin state. * * @param bool|string|array $account cached account data. * * @return bool True if the cached account is valid. */ public function is_valid_cached_account( $account ) { // null/false means no account has been cached. if ( null === $account || false === $account ) { return false; } // Non-array values are not expected. if ( ! is_array( $account ) ) { return false; } // Empty array - special value to indicate that there's no account connected. if ( empty( $account ) ) { return true; } // Live accounts are always valid. if ( $account['is_live'] ) { return true; } // Handle test accounts. // Fix test mode enabled DB state starting with the account data. // These two should be in sync when in test mode onboarding. // This is a weird case that shouldn't happen under normal circumstances. if ( ! WC_Payments_Onboarding_Service::is_test_mode_enabled() && WC_Payments::mode()->is_dev() ) { Logger::warning( 'Test mode account, account onboarding is NOT in test mode, but the plugin is in dev mode. Enabling test mode onboarding.' ); WC_Payments_Onboarding_Service::set_test_mode( true ); } // Test accounts are valid only when onboarding in test mode. if ( WC_Payments_Onboarding_Service::is_test_mode_enabled() ) { return true; } return false; } /** * Updates Stripe account settings. * * @param array $stripe_account_settings Settings to update. * * @return null|WP_Error Account update result. * * @throws Exception */ public function update_stripe_account( $stripe_account_settings ) { try { if ( ! $this->settings_changed( $stripe_account_settings ) ) { Logger::info( 'Skip updating account settings. Nothing is changed.' ); return; } $request = Update_Account::from_account_settings( $stripe_account_settings ); $response = $request->send(); $updated_account = $response->to_array(); $this->database_cache->add( Database_Cache::ACCOUNT_KEY, $updated_account ); } catch ( Exception $e ) { Logger::error( 'Failed to update Stripe account ' . $e ); return new WP_Error( 'wcpay_failed_to_update_stripe_account', $e->getMessage() ); } } /** * Checks if account settings changed. * * @param array $changes Account settings changes. * * @return bool True if at least one parameter value is changed. */ private function settings_changed( $changes = [] ) { $account = $this->database_cache->get( Database_Cache::ACCOUNT_KEY ); // Consider changes as valid if we don't have cached account data. if ( ! $this->is_valid_cached_account( $account ) ) { return true; } $diff = array_diff_assoc( $changes, $account ); return ! empty( $diff ); } /** * Updates the WCPay Account locale with the current site language (WPLANG option). * * @param string $option_name Option name. * @param mixed $old_value The old option value. * @param mixed $new_value The new option value. */ public function possibly_update_wcpay_account_locale( $option_name, $old_value, $new_value ) { if ( 'WPLANG' === $option_name && $this->is_stripe_connected() ) { try { $account_settings = [ 'locale' => $new_value ? $new_value : 'en_US', ]; $request = Update_Account::from_account_settings( $account_settings ); $response = $request->send(); $updated_account = $response->to_array(); $this->database_cache->add( Database_Cache::ACCOUNT_KEY, $updated_account ); } catch ( Exception $e ) { Logger::error( __( 'Failed to update Account locale. ', 'woocommerce-payments' ) . $e ); } } } /** * Retrieves the latest ToS agreement for the account. * * @return array|null Either the agreement or null if unavailable. */ public function get_latest_tos_agreement() { $account = $this->get_cached_account_data(); return ! empty( $account ) && isset( $account['latest_tos_agreement'] ) ? $account['latest_tos_agreement'] : null; } /** * Gets the account country. * * @return string Country. */ public function get_account_country() { $account = $this->get_cached_account_data(); return $account['country'] ?? Country_Code::UNITED_STATES; } /** * Gets the account default currency. * * @return string Currency code in lowercase. */ public function get_account_default_currency(): string { $account = $this->get_cached_account_data(); return $account['store_currencies']['default'] ?? strtolower( Currency_Code::UNITED_STATES_DOLLAR ); } /** * Handles adding a note if the merchant is eligible for Instant Deposits. * * @param array $account The account data. * * @return void */ public function handle_instant_deposits_inbox_note( $account ) { if ( empty( $account ) ) { return; } if ( ! $this->is_instant_deposits_eligible( $account ) ) { return; } // Track that this merchant has been eligible for instant deposits. // Used to show an informative notice if they later become ineligible. update_option( 'wcpay_instant_deposits_previously_eligible', true ); require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-instant-deposits-eligible.php'; WC_Payments_Notes_Instant_Deposits_Eligible::possibly_add_note(); $this->maybe_add_instant_deposit_note_reminder(); } /** * Handles adding a note if the merchant has an loan approved. * * @param array $account The account data. * * @return void */ public function handle_loan_approved_inbox_note( $account ) { require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-loan-approved.php'; // If the account cache is empty, don't try to create an inbox note. if ( empty( $account ) ) { return; } // Delete the loan note when the user doesn't have an active loan. if ( ! isset( $account['capital']['has_active_loan'] ) || ! $account['capital']['has_active_loan'] ) { WC_Payments_Notes_Loan_Approved::possibly_delete_note(); return; } // Get the loan summary. try { $request = Request::get( WC_Payments_API_Client::CAPITAL_API . '/active_loan_summary' ); $request->assign_hook( 'wcpay_get_active_loan_summary_request' ); $loan_details = $request->send(); } catch ( API_Exception $ex ) { return; } WC_Payments_Notes_Loan_Approved::set_loan_details( $loan_details ); WC_Payments_Notes_Loan_Approved::possibly_add_note(); } /** * Handles removing note about merchant Instant Deposits eligibility. * Hands off to handle_instant_deposits_inbox_note to add the new note. * * @return void */ public function handle_instant_deposits_inbox_reminder() { require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-instant-deposits-eligible.php'; WC_Payments_Notes_Instant_Deposits_Eligible::possibly_delete_note(); $this->handle_instant_deposits_inbox_note( $this->get_cached_account_data() ); } /** * Handles adding scheduled action for the Instant Deposit note reminder. * * @return void */ public function maybe_add_instant_deposit_note_reminder() { $action_hook = self::INSTANT_DEPOSITS_REMINDER_ACTION; if ( $this->action_scheduler_service->pending_action_exists( $action_hook ) ) { return; } $reminder_time = time() + ( 90 * DAY_IN_SECONDS ); $this->action_scheduler_service->schedule_job( $reminder_time, $action_hook ); } /** * Checks to see if the account is eligible for Instant Deposits. * * @param array $account The account data. * * @return bool */ private function is_instant_deposits_eligible( array $account ): bool { if ( empty( $account['instant_deposits_eligible'] ) ) { return false; } return true; } /** * Get card testing protection eligible flag account * * @return bool */ public function is_card_testing_protection_eligible(): bool { $account = $this->get_cached_account_data(); return $account['card_testing_protection_eligible'] ?? false; } /** * Gather the latest store setup state and send it to the Transact Platform. * * @return void */ public function store_setup_sync() { if ( ! $this->payments_api_client->is_server_connected() ) { return; } try { // This is a fire-and-forget operation, so we don't care about the result. $this->payments_api_client->send_store_setup( $this->get_store_setup_details() ); } catch ( Throwable $e ) { Logger::error( 'Failed to sync store setup state with the Transact Platform: ' . $e->getMessage() ); } } /** * Gathers the current store setup details. * * This overlaps heavily with the extension settings, but it is not limited to it. * * @see WC_REST_Payments_Settings_Controller::get_settings(). * * @return array Store setup details. * @throws Exception In case things are not properly initialized yet. */ private function get_store_setup_details(): array { $gateway = WC_Payments::get_gateway(); // If the gateway is not available, return an empty array. // This should never happen, but better safe than sorry. if ( empty( $gateway ) || ! $gateway instanceof WC_Payment_Gateway_WCPay ) { return []; } $gateway_form_fields = $gateway->get_form_fields(); $payment_methods_available = $gateway->get_upe_available_payment_methods(); $payment_methods_enabled = $gateway->get_upe_enabled_payment_method_ids(); $payment_methods_disabled = array_values( array_diff( $payment_methods_available, $payment_methods_enabled ) ); // Map enabled payment methods to capabilities. // This is needed because the capabilities in the Transact Platform are named differently. // E.g. 'card_payments' capability corresponds to 'card' payment method. $provider_capabilities_enabled = []; $provider_capabilities_disabled = []; $pm_to_capability_key_map = $gateway->get_payment_method_capability_key_map(); foreach ( $payment_methods_enabled as $pm_id ) { if ( isset( $pm_to_capability_key_map[ $pm_id ] ) ) { $provider_capabilities_enabled[] = $pm_to_capability_key_map[ $pm_id ]; } } foreach ( $payment_methods_disabled as $pm_id ) { if ( isset( $pm_to_capability_key_map[ $pm_id ] ) ) { $provider_capabilities_disabled[] = $pm_to_capability_key_map[ $pm_id ]; } } $provider_capabilities_available = array_unique( array_merge( $provider_capabilities_enabled, $provider_capabilities_disabled ) ); return [ // The WooPayments setup details. 'gateway' => [ 'enabled' => $gateway->is_enabled(), 'test_mode' => WC_Payments::mode()->is_test(), 'test_mode_onboarding' => WC_Payments::mode()->is_test_mode_onboarding(), ], // Payment methods setup. 'payment_methods' => [ 'available' => $payment_methods_available, 'enabled' => $payment_methods_enabled, 'disabled' => $payment_methods_disabled, 'duplicates' => $gateway->find_duplicates(), ], // Payment methods mapped to capabilities, for flexibility with the Transact Platform. // E.g. 'card_payments' capability corresponds to 'card' payment method. 'provider_capabilities' => [ 'available' => $provider_capabilities_available, 'enabled' => $provider_capabilities_enabled, 'disabled' => $provider_capabilities_disabled, ], 'apple_google_pay_in_payment_methods_options_enabled' => $gateway->get_option( 'apple_google_pay_in_payment_methods_options' ), 'saved_cards_enabled' => $gateway->is_saved_cards_enabled(), 'manual_capture_enabled' => 'yes' === $gateway->get_option( 'manual_capture' ), 'debug_log_enabled' => 'yes' === $gateway->get_option( 'enable_logging' ), 'payment_request' => [ 'enabled' => $gateway->is_payment_request_enabled(), 'enabled_locations' => $this->get_express_checkout_method_locations( $gateway, 'payment_request' ), 'button_type' => $gateway->get_option( 'payment_request_button_type' ), 'button_size' => $gateway->get_option( 'payment_request_button_size' ), 'button_theme' => $gateway->get_option( 'payment_request_button_theme' ), 'button_border_radius' => $gateway->get_option( 'payment_request_button_border_radius' ), ], 'woopay' => [ 'enabled' => WC_Payments_Features::is_woopay_enabled(), 'enabled_locations' => $this->get_express_checkout_method_locations( $gateway, 'woopay' ), 'store_logo' => $gateway->get_option( 'platform_checkout_store_logo' ), 'custom_message' => $gateway->get_option( 'platform_checkout_custom_message' ), 'invalid_extension_found' => (bool) get_option( 'woopay_invalid_extension_found', false ), ], // WooPayments features. 'multi_currency_enabled' => WC_Payments_Features::is_customer_multi_currency_enabled(), 'stripe_billing_enabled' => WC_Payments_Features::is_stripe_billing_enabled(), // Other WooPayments details. 'plugin' => [ 'version' => defined( 'WCPAY_VERSION_NUMBER' ) ? explode( '-', WCPAY_VERSION_NUMBER, 2 )[0] : '', 'activation_timestamp' => get_option( 'wcpay_activation_timestamp', null ), ], // Other store setup details. 'wp_setup' => [ 'name' => get_bloginfo( 'name' ), 'url' => home_url(), 'active_theme' => $this->get_store_theme_details(), 'active_plugins' => $this->get_store_active_plugins(), 'version' => get_bloginfo( 'version' ), 'locale' => get_locale(), ], 'wc_setup' => [ 'version' => defined( 'WC_VERSION' ) ? explode( '-', WC_VERSION, 2 )[0] : '', 'store_id' => ( class_exists( '\WC_Install' ) && defined( '\WC_Install::STORE_ID_OPTION' ) ) ? get_option( \WC_Install::STORE_ID_OPTION, null ) : null, 'currency' => get_woocommerce_currency(), 'tracking_enabled' => WC_Site_Tracking::is_tracking_enabled(), 'registered_payment_gateways' => $this->get_store_registered_gateway_ids(), 'enabled_payment_gateways' => $this->get_store_enabled_gateway_ids(), 'wc_subscriptions_active' => $gateway->is_subscriptions_plugin_active(), 'wc_subscriptions_version' => $gateway->get_subscriptions_plugin_version(), ], ]; } /** * Gathers the current store theme details. * * @return array Store theme details. */ private function get_store_theme_details(): array { $theme_data = wp_get_theme(); return [ 'name' => $theme_data->Name, // @phpcs:ignore 'version' => $theme_data->Version, // @phpcs:ignore 'child_theme' => is_child_theme(), 'wc_support' => current_theme_supports( 'woocommerce' ), 'block_theme' => wp_is_block_theme(), ]; } /** * Gathers the current store active (and valid) plugins. * * @return array Store active plugins details with each plugin slug and version. */ private function get_store_active_plugins(): array { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $all_plugins = get_plugins(); if ( empty( $all_plugins ) ) { return []; } // Get active plugins using the PluginUtil from WC, if available. $wc_plugin_util = null; if ( class_exists( '\Automattic\WooCommerce\Utilities\PluginUtil' ) ) { try { $wc_plugin_util = wc_get_container()->get( '\Automattic\WooCommerce\Utilities\PluginUtil' ); } catch ( Throwable $e ) { // If we can't get the PluginUtil, we won't be able to accurately get the active plugins. // This is not a critical failure, so we can log it and continue. Logger::error( 'Failed to get PluginUtil: ' . $e->getMessage() ); } } $plugins_list = []; $active_plugin_ids = ( is_object( $wc_plugin_util ) && is_callable( [ $wc_plugin_util, 'get_all_active_valid_plugins' ] ) ) ? $wc_plugin_util->get_all_active_valid_plugins() : wp_get_active_and_valid_plugins(); foreach ( $active_plugin_ids as $plugin_file ) { if ( isset( $all_plugins[ $plugin_file ] ) ) { $plugin_data = $all_plugins[ $plugin_file ]; $plugins_list[ $plugin_file ] = [ 'name' => $plugin_data['Name'], 'slug' => dirname( $plugin_file ), 'version' => $plugin_data['Version'], 'wc_aware' => ( is_object( $wc_plugin_util ) && is_callable( [ $wc_plugin_util, 'is_woocommerce_aware_plugin' ] ) ) ? $wc_plugin_util->is_woocommerce_aware_plugin( $plugin_data ) : null, ]; } } return array_values( $plugins_list ); } /** * Gets the IDs of all payment gateways registered in the store. * * @return array Array of payment gateway IDs. */ private function get_store_registered_gateway_ids(): array { $payment_gateways = WC()->payment_gateways()->payment_gateways(); if ( empty( $payment_gateways ) ) { return []; } // Go through the gateways and get their IDs. return array_unique( array_values( array_filter( array_map( function ( $gateway ) { return $gateway->id ?? null; }, $payment_gateways ) ) ) ); } /** * Gets the IDs of all enabled payment gateways registered in the store. * * @return array Array of enabled payment gateway IDs. */ private function get_store_enabled_gateway_ids(): array { $payment_gateways = WC()->payment_gateways()->payment_gateways(); if ( empty( $payment_gateways ) ) { return []; } // Go through the gateways and get the IDs of enabled ones. return array_unique( array_values( array_filter( array_map( function ( $gateway ) { return ( $gateway instanceof WC_Payment_Gateway && wc_string_to_bool( $gateway->enabled ) ) ? $gateway->id : null; }, $payment_gateways ) ) ) ); } /** * Gets the locations where a specific express checkout method is enabled. * * Derives the locations from the location-centric express checkout settings. * * @param WC_Payment_Gateway_WCPay $gateway The WCPay gateway instance. * @param string $method_id The method identifier (e.g., 'payment_request', 'woopay'). * * @return array Array of location identifiers where the method is enabled. */ private function get_express_checkout_method_locations( WC_Payment_Gateway_WCPay $gateway, string $method_id ): array { $locations = [ 'product', 'cart', 'checkout' ]; $enabled_locations = []; foreach ( $locations as $location ) { $enabled_methods = $gateway->get_option( "express_checkout_{$location}_methods", [] ); if ( is_array( $enabled_methods ) && in_array( $method_id, $enabled_methods, true ) ) { $enabled_locations[] = $location; } } return $enabled_locations; } /** * Gets tracking info from the server and caches it. * * It's only available after connecting to Jetpack, so we should only cache it after that. * * @param bool $force_refresh Whether to force a refresh of the tracking info. * * @return array|null Array of tracking info or null if unavailable. */ public function get_tracking_info( $force_refresh = false ): ?array { if ( ! $this->payments_api_client->is_server_connected() ) { return null; } return $this->database_cache->get_or_add( Database_Cache::TRACKING_INFO_KEY, function (): array { return $this->payments_api_client->get_tracking_info(); }, 'is_array', // We expect an array back from the cache. $force_refresh ); } /** * Temporarily store the test drive account settings. * * If the current account is a test-drive account, * we need to collect the test drive settings before we delete the test-drive account, * and apply those settings to the live account. * * @return void */ public function save_test_drive_settings(): void { $account = $this->get_cached_account_data(); if ( ! empty( $account['is_test_drive'] ) && true === $account['is_test_drive'] ) { $test_drive_account_data = $this->get_test_drive_settings_for_live_account(); // Store the test drive settings for the live account in a transient, // We don't pass the data around, as the merchant might cancel and start // the onboarding from scratch. In this case, we won't have the test drive // account anymore to collect the settings. set_transient( self::ONBOARDING_TEST_DRIVE_SETTINGS_FOR_LIVE_ACCOUNT, $test_drive_account_data, HOUR_IN_SECONDS ); } } /** * Send a Tracks event. * * By default Woo adds `url`, `blog_lang`, `blog_id`, `store_id`, `products_count`, and `wc_version` * properties to every event. * * @param string $name The event name. * @param array $properties Optional. The event custom properties. * * @return void */ private function tracks_event( string $name, array $properties = [] ) { if ( ! function_exists( 'wc_admin_record_tracks_event' ) ) { return; } // Add default properties to every event. $properties = array_merge( $properties, [ 'is_test_mode' => WC_Payments::mode()->is_test(), 'jetpack_connected' => $this->payments_api_client->is_server_connected(), 'wcpay_version' => WCPAY_VERSION_NUMBER, 'woo_country_code' => WC()->countries->get_base_country(), ], $this->get_tracking_info() ?? [] ); // We're not using Tracker::track_admin() here because // WC_Pay\record_tracker_events() is never triggered due to the redirects. wc_admin_record_tracks_event( $name, $properties ); Logger::info( 'Tracks event: ' . $name . ' with data: ' . wp_json_encode( WC_Payments_Utils::redact_array( $properties, [ 'woo_country_code' ] ) ) ); } /** * Get the all-time total payment volume. * * @return int The all-time total payment volume, or null if not available. */ public function get_lifetime_total_payment_volume(): int { $account = $this->get_cached_account_data(); return (int) ! empty( $account ) && isset( $account['lifetime_total_payment_volume'] ) ? $account['lifetime_total_payment_volume'] : 0; } /** * Retrieve the embedded account session. * * Will return the session key used to initialise the embedded session. * * @return array Session data. * * @throws API_Exception|Exception */ public function create_embedded_account_session(): array { if ( ! $this->payments_api_client->is_server_connected() ) { return []; } try { $account_session = $this->payments_api_client->create_embedded_account_session(); } catch ( API_Exception $e ) { // If we fail to create the session, return an empty array. return []; } return [ 'clientSecret' => $account_session['client_secret'] ?? '', 'expiresAt' => $account_session['expires_at'] ?? 0, 'accountId' => $account_session['account_id'] ?? '', 'isLive' => $account_session['is_live'] ?? false, 'publishableKey' => $account_session['publishable_key'] ?? '', ]; } /** * Extract the useful test drive settings from the account data. * * We will use this data to migrate the test drive settings when onboarding the live account. * ATM we only store the enabled payment methods. * * @return array The test drive settings for the live account. */ private function get_test_drive_settings_for_live_account(): array { $gateway = WC_Payments::get_gateway(); $capabilities = []; foreach ( $gateway->get_upe_enabled_payment_method_ids() as $payment_method_id ) { $capabilities[ $payment_method_id . '_payments' ] = [ 'requested' => 'true' ]; } return [ 'capabilities' => $capabilities ]; } } includes/class-duplicates-detection-service.php 0000777 00000014713 15251156605 0015760 0 ustar 00 <?php /** * Class Duplicates_Detection_Service * * @package WooCommerce\Payments */ namespace WCPay; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WC_Payments; use WCPay\Payment_Methods\CC_Payment_Method; use WCPay\PaymentMethods\Configs\Registry\PaymentMethodDefinitionRegistry; /** * Class handling detection of payment methods enabled by multiple plugins simultaneously. */ class Duplicates_Detection_Service { /** * Registered gateways. * * @var array */ private $registered_gateways = null; /** * Gateways qualified by duplicates detector. * * @var array */ private $gateways_qualified_by_duplicates_detector = []; /** * Find duplicates. * * @return array Duplicated gateways. */ public function find_duplicates() { try { $this->gateways_qualified_by_duplicates_detector = []; $this->search_for_cc() ->search_for_additional_payment_methods() ->search_for_payment_request_buttons() ->keep_gateways_enabled_in_woopayments() ->keep_duplicates_only(); // Return payment method IDs list so that front-end can successfully compare with its own list. return $this->gateways_qualified_by_duplicates_detector; } catch ( \Exception $e ) { Logger::warning( 'Duplicates detection service failed silently with the following error: ' . $e->getMessage() ); // Fail silently and return an empty array in case of any exception. return []; } } /** * Search for credit card gateways. * * @return Duplicates_Detection_Service */ private function search_for_cc() { $keywords = [ 'credit_card', 'creditcard', 'cc', 'card' ]; $special_keywords = [ 'woocommerce_payments', 'stripe' ]; foreach ( $this->get_enabled_gateways() as $gateway ) { if ( $this->gateway_contains_keyword( $gateway->id, $keywords ) || in_array( $gateway->id, $special_keywords, true ) ) { $this->gateways_qualified_by_duplicates_detector[ CC_Payment_Method::PAYMENT_METHOD_STRIPE_ID ][] = $gateway->id; } } return $this; } /** * Search for additional payment methods. * * @return Duplicates_Detection_Service */ private function search_for_additional_payment_methods() { /** * FLAG: PAYMENT_METHODS_LIST * As payment methods are converted to use definitions, they need to be removed from the list below. */ $keywords = []; // Get all payment method definitions. $payment_method_definitions = PaymentMethodDefinitionRegistry::instance()->get_all_payment_method_definitions(); // This gets all the registered payment method definitions. As new payment methods are converted from the legacy style, they need to be removed from the list above. foreach ( $payment_method_definitions as $definition_class ) { $definition_keywords = $definition_class::get_keywords(); foreach ( $definition_keywords as $keyword ) { $keywords[ $keyword ] = $definition_class::get_id(); } } foreach ( $this->get_enabled_gateways() as $gateway ) { foreach ( $keywords as $keyword => $payment_method ) { if ( strpos( $gateway->id, $keyword ) !== false ) { $this->gateways_qualified_by_duplicates_detector[ $payment_method ][] = $gateway->id; break; } } } return $this; } /** * Search for payment request buttons. * * @return Duplicates_Detection_Service */ private function search_for_payment_request_buttons() { $prb_payment_method = 'apple_pay_google_pay'; $keywords = [ 'apple_pay', 'applepay', 'google_pay', 'googlepay', ]; foreach ( $this->get_registered_gateways() as $gateway ) { if ( 'yes' === $gateway->enabled ) { foreach ( $keywords as $keyword ) { if ( strpos( $gateway->id, $keyword ) !== false ) { $this->gateways_qualified_by_duplicates_detector[ $prb_payment_method ][] = $gateway->id; break; } elseif ( 'woocommerce_payments' === $gateway->id && method_exists( $gateway, 'is_payment_request_enabled' ) && $gateway->is_payment_request_enabled() ) { $this->gateways_qualified_by_duplicates_detector[ $prb_payment_method ][] = $gateway->id; break; } elseif ( 'stripe' === $gateway->id && 'yes' === $gateway->get_option( 'payment_request' ) ) { $this->gateways_qualified_by_duplicates_detector[ $prb_payment_method ][] = $gateway->id; break; } elseif ( 'yes' === $gateway->get_option( 'express_checkout_enabled' ) ) { $this->gateways_qualified_by_duplicates_detector[ $prb_payment_method ][] = $gateway->id; break; } } } } return $this; } /** * Keep only WooCommerce Payments enabled gateways. * * @return Duplicates_Detection_Service */ private function keep_gateways_enabled_in_woopayments() { $woopayments_gateway_ids = array_map( function ( $gateway ) { return $gateway->id; }, array_values( WC_Payments::get_payment_gateway_map() ) ); foreach ( $this->gateways_qualified_by_duplicates_detector as $gateway_id => $gateway_ids ) { if ( empty( array_intersect( $gateway_ids, $woopayments_gateway_ids ) ) ) { unset( $this->gateways_qualified_by_duplicates_detector[ $gateway_id ] ); } } return $this; } /** * Filter payment methods found to keep duplicates only. * * @return Duplicates_Detection_Service */ private function keep_duplicates_only() { foreach ( $this->gateways_qualified_by_duplicates_detector as $gateway_id => $gateway_ids ) { if ( count( $gateway_ids ) < 2 ) { unset( $this->gateways_qualified_by_duplicates_detector[ $gateway_id ] ); } } return $this; } /** * Filter enabled gateways only. * * @return array Enabled gateways only. */ private function get_enabled_gateways() { return array_filter( $this->get_registered_gateways(), function ( $gateway ) { return 'yes' === $gateway->enabled; } ); } /** * Check if gateway ID contains any of the keywords. * * @param string $gateway_id Gateway ID. * @param array $keywords Keywords to search for. * * @return bool True if gateway ID contains any of the keywords, false otherwise. */ private function gateway_contains_keyword( $gateway_id, $keywords ) { foreach ( $keywords as $keyword ) { if ( strpos( $gateway_id, $keyword ) !== false ) { return true; } } return false; } /** * Lazy load registered gateways. * * @return array Registered gateways. */ private function get_registered_gateways() { if ( null === $this->registered_gateways ) { $this->registered_gateways = WC()->payment_gateways->payment_gateways(); } return $this->registered_gateways; } } includes/class-wc-payments-payment-request-session-handler.php 0000777 00000015075 15251156605 0020717 0 ustar 00 <?php /** * Class WC_Payments_Payment_Request_Session_Handler * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken; /** * WC_Payments_Payment_Request_Session_Handler class */ final class WC_Payments_Payment_Request_Session_Handler extends WC_Session_Handler { /** * Token from HTTP headers. * * @var string */ protected $token; /** * The session id to reference in the sessions table. * * @var string */ public $session_id; /** * Expiration timestamp. * * @var int */ protected $session_expiration; /** * Constructor for the session class. */ public function __construct() { parent::__construct(); $this->token = wc_clean( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION'] ?? '' ) ); } /** * Init hooks and session data. * * @throws Exception On possible token mismatch. */ public function init() { $this->init_session_cookie(); if ( $this->_customer_id !== $this->_data['token_customer_id'] ) { \WCPay\Logger::error( sprintf( 'Tokenized ECE cookie and session customer mismatch - customer: %s (%s) , session: %s (%s)', var_export( $this->_customer_id, true ), // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- should only be triggered when logging is enabled. gettype( $this->_customer_id ), var_export( $this->_data['token_customer_id'], true ), // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- should only be triggered when logging is enabled. gettype( $this->_data['token_customer_id'] ) ) ); // throwing an exception here to prevent further processing of the request. throw new Exception( __( 'Invalid token: cookie and session customer mismatch', 'woocommerce-payments' ) ); } // saving the session and sending new cookies only when we're not dealing with an ephemeral cart. if ( wc_clean( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_IS_EPHEMERAL_CART'] ?? '' ) ) !== '1' ) { add_action( 'shutdown', [ $this, 'save_data' ], 20 ); add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_cookie' ] ); } } /** * Setup cookie and customer ID. * We need to ensure that we _also_ call `init_session_from_token` when `init_session_cookie` is called. * Otherwise, this clears everything: https://github.com/woocommerce/woocommerce/blob/de4a8ffdd474ca1879d4aa16487d6c52472a861b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php#L556-L558 */ public function init_session_cookie() { // If an account has been created after the session has been initialized, update the session. // This method is called directly by WC blocks when an account is created right before placing an order. $previous_session_data = null; parent::init_session_cookie(); if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) { $previous_session_data = $this->_data; $this->_customer_id = strval( get_current_user_id() ); } $this->init_session_from_token(); if ( ! empty( $previous_session_data ) ) { $this->_data = $previous_session_data; } } /** * Process the token header to load the correct session. */ protected function init_session_from_token() { $default_value = [ 'token_customer_id' => $this->_customer_id, ]; if ( empty( $this->token ) ) { $this->session_id = $this->generate_customer_id(); $this->_data = $default_value; // session_expiration can remain the same. return; } $payload = JsonWebToken::get_parts( $this->token )->payload; $this->session_id = $payload->session_id; $this->session_expiration = $payload->exp; $this->_data = (array) $this->get_session( $this->session_id, $default_value ); } /** * Delete the session from the cache and database. * * @param int $customer_id Customer ID. */ public function delete_session( $customer_id ) { parent::delete_session( $this->session_id ); } /** * Update the session expiry timestamp. * * @param string $customer_id Customer ID. * @param int $timestamp Timestamp to expire the cookie. */ public function update_session_timestamp( $customer_id, $timestamp ) { parent::update_session_timestamp( $this->session_id, $timestamp ); } /** * Forget all session data without destroying it. */ public function forget_session() { parent::forget_session(); $this->session_id = $this->generate_customer_id(); } /** * Generate a unique customer ID for both logged-in and guest customers. * * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID. * * @return string */ public function generate_customer_id() { require_once ABSPATH . 'wp-includes/class-phpass.php'; $hasher = new PasswordHash( 8, false ); return 't_' . substr( md5( $hasher->get_random_bytes( 32 ) ), 2 ); } /** * Save data - copy of parent method with a few modifications. * * @param int $old_session_key session ID before user logs in. */ public function save_data( $old_session_key = 0 ) { // Dirty if something changed - prevents saving nothing new. if ( $this->_dirty ) { global $wpdb; $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "INSERT INTO $this->_table (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d) ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)", $this->session_id, maybe_serialize( $this->_data ), $this->_session_expiration ) ); wp_cache_set( $this->get_cache_prefix() . $this->session_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() ); $this->_dirty = false; } } /** * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call. * * @return string */ private function get_cache_prefix() { return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP ); } /** * Get a session variable. * Overridden default method, so that the `cart` session data always returns a value, in order to prevent the "saved cart after login" feature to get wrong cart data. * See "WC_Cart_Session::get_cart_from_session". * * @param string $key Key to get. * @param mixed $default used if the session variable isn't set. * @return array|string value of session variable */ public function get( $key, $default = null ) { if ( 'cart' === $key && ! isset( $this->_data['cart'] ) ) { return []; } return parent::get( $key, $default ); } } includes/constants/class-country-test-cards.php 0000777 00000010660 15251156605 0015774 0 ustar 00 <?php /** * Class Country_Test_Cards * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Class handling country-specific test card numbers for WooPayments */ class Country_Test_Cards extends Base_Constant { /** * Map of country codes to their test card numbers * Source: https://docs.stripe.com/testing?testing-method=card-numbers#international-cards * * @var array */ private static $country_test_cards = [ Country_Code::UNITED_STATES => '4242 4242 4242 4242', Country_Code::ARGENTINA => '4000 0003 2000 0021', Country_Code::BRAZIL => '4000 0007 6000 0002', Country_Code::CANADA => '4000 0012 4000 0000', Country_Code::CHILE => '4000 0015 2000 0001', Country_Code::COLOMBIA => '4000 0017 0000 0003', Country_Code::COSTA_RICA => '4000 0018 8000 0005', Country_Code::ECUADOR => '4000 0021 8000 0000', Country_Code::MEXICO => '4000 0048 4000 8001', Country_Code::PANAMA => '4000 0059 1000 0000', Country_Code::PARAGUAY => '4000 0060 0000 0066', Country_Code::PERU => '4000 0060 4000 0068', Country_Code::URUGUAY => '4000 0085 8000 0003', Country_Code::UNITED_ARAB_EMIRATES => '4000 0078 4000 0001', Country_Code::AUSTRIA => '4000 0004 0000 0008', Country_Code::BELGIUM => '4000 0005 6000 0004', Country_Code::BULGARIA => '4000 0010 0000 0000', Country_Code::BELARUS => '4000 0011 2000 0005', Country_Code::CROATIA => '4000 0019 1000 0009', Country_Code::CYPRUS => '4000 0019 6000 0008', Country_Code::CZECHIA => '4000 0020 3000 0002', Country_Code::DENMARK => '4000 0020 8000 0001', Country_Code::ESTONIA => '4000 0023 3000 0009', Country_Code::FINLAND => '4000 0024 6000 0001', Country_Code::FRANCE => '4000 0025 0000 0003', Country_Code::GERMANY => '4000 0027 6000 0016', Country_Code::GIBRALTAR => '4000 0029 2000 0005', Country_Code::GREECE => '4000 0030 0000 0030', Country_Code::HUNGARY => '4000 0034 8000 0005', Country_Code::IRELAND => '4000 0037 2000 0005', Country_Code::ITALY => '4000 0038 0000 0008', Country_Code::LATVIA => '4000 0042 8000 0005', Country_Code::LIECHTENSTEIN => '4000 0043 8000 0004', Country_Code::LITHUANIA => '4000 0044 0000 0000', Country_Code::LUXEMBOURG => '4000 0044 2000 0006', Country_Code::MALTA => '4000 0047 0000 0007', Country_Code::NETHERLANDS => '4000 0052 8000 0002', Country_Code::NORWAY => '4000 0057 8000 0007', Country_Code::POLAND => '4000 0061 6000 0005', Country_Code::PORTUGAL => '4000 0062 0000 0007', Country_Code::ROMANIA => '4000 0064 2000 0001', Country_Code::SAUDI_ARABIA => '4000 0068 2000 0007', Country_Code::SLOVENIA => '4000 0070 5000 0006', Country_Code::SLOVAKIA => '4000 0070 3000 0001', Country_Code::SPAIN => '4000 0072 4000 0007', Country_Code::SWEDEN => '4000 0075 2000 0008', Country_Code::SWITZERLAND => '4000 0075 6000 0009', Country_Code::UNITED_KINGDOM => '4000 0082 6000 0000', Country_Code::AUSTRALIA => '4000 0003 6000 0006', Country_Code::CHINA => '4000 0015 6000 0002', Country_Code::HONG_KONG => '4000 0034 4000 0004', Country_Code::INDIA => '4000 0035 6000 0008', Country_Code::JAPAN => '4000 0039 2000 0003', Country_Code::MALAYSIA => '4000 0045 8000 0002', Country_Code::NEW_ZEALAND => '4000 0055 4000 0008', Country_Code::SINGAPORE => '4000 0070 2000 0003', Country_Code::TAIWAN => '4000 0015 8000 0008', Country_Code::THAILAND => '4000 0076 4000 0003', ]; /** * Get test card number for a specific country. * * @param string $country_code Two-letter country code. * @return string Test card number */ public static function get_test_card_for_country( string $country_code ) { return self::$country_test_cards[ $country_code ] ?? self::$country_test_cards[ Country_Code::UNITED_STATES ]; } } includes/constants/class-order-status.php 0000777 00000001077 15251156605 0014660 0 ustar 00 <?php /** * Class Order_Status * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * This class gives a list of all the possible order status constants. * * @phpstan-immutable */ class Order_Status extends Base_Constant { const CANCELLED = 'cancelled'; const COMPLETED = 'completed'; const FAILED = 'failed'; const ON_HOLD = 'on-hold'; const PENDING = 'pending'; const PROCESSING = 'processing'; const REFUNDED = 'refunded'; const TRASH = 'trash'; } includes/constants/class-payment-type.php 0000777 00000000671 15251156605 0014657 0 ustar 00 <?php /** * Class Payment_Type * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Enum used in WCPay\Payment_Information to determine whether a payment * is a single payment or a part of a recurring series. * * @phpstan-immutable */ class Payment_Type extends Base_Constant { const SINGLE = 'single'; const RECURRING = 'recurring'; } includes/constants/class-currency-code.php 0000777 00000024557 15251156605 0014776 0 ustar 00 <?php /** * Class Currency_Code * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Currency Code constants * * @psalm-immutale */ class Currency_Code extends Base_Constant { const UNITED_STATES_DOLLAR = 'USD'; // United States Dollar. const UNITED_ARAB_EMIRATES_DIRHAM = 'AED'; // United Arab Emirates dirham. const AFGHAN_AFGHANI = 'AFN'; // Afghan afghani. const ALBANIAN_LEK = 'ALL'; // Albanian lek. const ARMENIAN_DRAM = 'AMD'; // Armenian dram. const NETHERLANDS_ANTILLEAN_GUILDER = 'ANG'; // Netherlands Antillean guilder. const ANGOLAN_KWANZA = 'AOA'; // Angolan kwanza. const ARGENTINE_PESO = 'ARS'; // Argentine peso. const AUSTRALIAN_DOLLAR = 'AUD'; // Australian dollar. const ARUBAN_FLORIN = 'AWG'; // Aruban florin. const AZERBAIJANI_MANAT = 'AZN'; // Azerbaijani manat. const BOSNIA_AND_HERZEGOVINA_CONVERTIBLE_MARK = 'BAM'; // Bosnia and Herzegovina convertible mark. const BARBADOS_DOLLAR = 'BBD'; // Barbados dollar. const BANGLADESHI_TAKA = 'BDT'; // Bangladeshi taka. const BULGARIAN_LEV = 'BGN'; // Bulgarian lev. const BURUNDIAN_FRANC = 'BIF'; // Burundian franc. const BERMUDIAN_DOLLAR = 'BMD'; // Bermudian dollar. const BRUNEI_DOLLAR = 'BND'; // Brunei dollar. const BOLIVIANO = 'BOB'; // Boliviano. const BRAZILIAN_REAL = 'BRL'; // Brazilian real. const BAHAMIAN_DOLLAR = 'BSD'; // Bahamian dollar. const BOTSWANA_PULA = 'BWP'; // Botswana pula. const NEW_BELARUSIAN_RUBLE = 'BYN'; // New Belarusian ruble. const BELIZE_DOLLAR = 'BZD'; // Belize dollar. const CANADIAN_DOLLAR = 'CAD'; // Canadian dollar. const CONGOLESE_FRANC = 'CDF'; // Congolese franc. const SWISS_FRANC = 'CHF'; // Swiss franc. const CHILEAN_PESO = 'CLP'; // Chilean peso. const CHINESE_YUAN = 'CNY'; // Renminbi (Chinese yuan). const COLOMBIAN_PESO = 'COP'; // Colombian peso. const COSTA_RICAN_COLON = 'CRC'; // Costa Rican colon. const CAPE_VERDE_ESCUDO = 'CVE'; // Cape Verde escudo. const CZECH_KORUNA = 'CZK'; // Czech koruna. const DJIBOUTIAN_FRANC = 'DJF'; // Djiboutian franc. const DANISH_KRONE = 'DKK'; // Danish krone. const DOMINICAN_PESO = 'DOP'; // Dominican peso. const ALGERIAN_DINAR = 'DZD'; // Algerian dinar. const EGYPTIAN_POUND = 'EGP'; // Egyptian pound. const ETHIOPIAN_BIRR = 'ETB'; // Ethiopian birr. const EURO = 'EUR'; // Euro. const FIJI_DOLLAR = 'FJD'; // Fiji dollar. const FALKLAND_ISLANDS_POUND = 'FKP'; // Falkland Islands pound. const POUND_STERLING = 'GBP'; // Pound sterling. const GEORGIAN_LARI = 'GEL'; // Georgian lari. const GIBRALTAR_POUND = 'GIP'; // Gibraltar pound. const GAMBIAN_DALASI = 'GMD'; // Gambian dalasi. const GUINEAN_FRANC = 'GNF'; // Guinean franc. const GUATEMALAN_QUETZAL = 'GTQ'; // Guatemalan quetzal. const GUYANESE_DOLLAR = 'GYD'; // Guyanese dollar. const HONG_KONG_DOLLAR = 'HKD'; // Hong Kong dollar. const HONDURAN_LEMPIRA = 'HNL'; // Honduran lempira. const HAITIAN_GOURDE = 'HTG'; // Haitian gourde. const HUNGARIAN_FORINT = 'HUF'; // Hungarian forint. const INDONESIAN_RUPIAH = 'IDR'; // Indonesian rupiah. const ISRAELI_NEW_SHEKEL = 'ILS'; // Israeli new shekel. const INDIAN_RUPEE = 'INR'; // Indian rupee. const ICELANDIC_KRONA = 'ISK'; // Icelandic króna. const JAMAICAN_DOLLAR = 'JMD'; // Jamaican dollar. const JAPANESE_YEN = 'JPY'; // Japanese yen. const KENYAN_SHILLING = 'KES'; // Kenyan shilling. const KYRGYZSTANI_SOM = 'KGS'; // Kyrgyzstani som. const CAMBODIAN_RIEL = 'KHR'; // Cambodian riel. const COMORIAN_FRANC = 'KMF'; // Comorian franc. const SOUTH_KOREAN_WON = 'KRW'; // South Korean won. const CAYMAN_ISLANDS_DOLLAR = 'KYD'; // Cayman Islands dollar. const KAZAKHSTANI_TENGE = 'KZT'; // Kazakhstani tenge. const LAO_KIP = 'LAK'; // Lao kip. const LEBANESE_POUND = 'LBP'; // Lebanese pound. const SRI_LANKAN_RUPEE = 'LKR'; // Sri Lankan rupee. const LIBERIAN_DOLLAR = 'LRD'; // Liberian dollar. const LESOTHO_LOTI = 'LSL'; // Lesotho loti. const MOROCCAN_DIRHAM = 'MAD'; // Moroccan dirham. const MOLDOVAN_LEU = 'MDL'; // Moldovan leu. const MALAGASY_ARIARY = 'MGA'; // Malagasy ariary. const MACEDONIAN_DENAR = 'MKD'; // Macedonian denar. const MYANMAR_KYAT = 'MMK'; // Myanmar kyat. const MONGOLIAN_TOGROG = 'MNT'; // Mongolian tögrög. const MACANESE_PATACA = 'MOP'; // Macanese pataca. const MAURITIAN_RUPEE = 'MUR'; // Mauritian rupee. const MALDIVIAN_RUFIYAA = 'MVR'; // Maldivian rufiyaa. const MALAWIAN_KWACHA = 'MWK'; // Malawian kwacha. const MEXICAN_PESO = 'MXN'; // Mexican peso. const MALAYSIAN_RINGGIT = 'MYR'; // Malaysian ringgit. const MOZAMBICAN_METICAL = 'MZN'; // Mozambican metical. const NAMIBIAN_DOLLAR = 'NAD'; // Namibian dollar. const NIGERIAN_NAIRA = 'NGN'; // Nigerian naira. const NICARAGUAN_CORDOBA = 'NIO'; // Nicaraguan córdoba. const NORWEGIAN_KRONE = 'NOK'; // Norwegian krone. const NEPALESE_RUPEE = 'NPR'; // Nepalese rupee. const NEW_ZEALAND_DOLLAR = 'NZD'; // New Zealand dollar. const PANAMANIAN_BALBOA = 'PAB'; // Panamanian balboa. const PERUVIAN_SOL = 'PEN'; // Peruvian sol. const PAPUA_NEW_GUINEAN_KINA = 'PGK'; // Papua New Guinean kina. const PHILIPPINE_PESO = 'PHP'; // Philippine peso. const PAKISTANI_RUPEE = 'PKR'; // Pakistani rupee. const POLISH_ZLOTY = 'PLN'; // Polish złoty. const PARAGUAYAN_GUARANI = 'PYG'; // Paraguayan guaraní. const QATARI_RIYAL = 'QAR'; // Qatari riyal. const ROMANIAN_LEU = 'RON'; // Romanian leu. const SERBIAN_DINAR = 'RSD'; // Serbian dinar. const RUSSIAN_RUBLE = 'RUB'; // Russian ruble. const RWANDAN_FRANC = 'RWF'; // Rwandan franc. const SAUDI_RIYAL = 'SAR'; // Saudi riyal. const SOLOMON_ISLANDS_DOLLAR = 'SBD'; // Solomon Islands dollar. const SEYCHELLOIS_RUPEE = 'SCR'; // Seychellois rupee. const SWEDISH_KRONA = 'SEK'; // Swedish krona. const SINGAPORE_DOLLAR = 'SGD'; // Singapore dollar. const SAINT_HELENA_POUND = 'SHP'; // Saint Helena pound. const SIERRA_LEONEAN_LEONE = 'SLE'; // Sierra Leonean leone. const SOMALI_SHILLING = 'SOS'; // Somali shilling. const SURINAMESE_DOLLAR = 'SRD'; // Surinamese dollar. const SAO_TOME_AND_PRINCIPE_DOBRA = 'STD'; // São Tomé and Príncipe dobra. const SWAZI_LILANGENI = 'SZL'; // Swazi lilangeni. const THAI_BAHT = 'THB'; // Thai baht. const TAJIKISTANI_SOMONI = 'TJS'; // Tajikistani somoni. const TONGAN_PAANGA = 'TOP'; // Tongan paʻanga. const TURKISH_LIRA = 'TRY'; // Turkish lira. const TRINIDAD_AND_TOBAGO_DOLLAR = 'TTD'; // Trinidad and Tobago dollar. const NEW_TAIWAN_DOLLAR = 'TWD'; // New Taiwan dollar. const TANZANIAN_SHILLING = 'TZS'; // Tanzanian shilling. const UKRAINIAN_HRYVNIA = 'UAH'; // Ukrainian hryvnia. const UGANDAN_SHILLING = 'UGX'; // Ugandan shilling. const URUGUAYAN_PESO = 'UYU'; // Uruguayan peso. const UZBEKISTANI_SOM = 'UZS'; // Uzbekistani som. const VIETNAMESE_DONG = 'VND'; // Vietnamese đồng. const VANUATU_VATU = 'VUV'; // Vanuatu vatu. const SAMOAN_TALA = 'WST'; // Samoan tala. const CENTRAL_AFRICAN_CFA_FRANC = 'XAF'; // Central African CFA franc. const EAST_CARIBBEAN_DOLLAR = 'XCD'; // East Caribbean dollar. const WEST_AFRICAN_CFA_FRANC = 'XOF'; // West African CFA franc. const CFP_FRANC = 'XPF'; // CFP franc. const YEMENI_RIAL = 'YER'; // Yemeni rial. const SOUTH_AFRICAN_RAND = 'ZAR'; // South African rand. const ZAMBIAN_KWACHA = 'ZMW'; // Zambian kwacha. // ... add more currencies as needed. // crypto currencies. const BITCOIN = 'BTC'; // Bitcoin. } includes/constants/class-base-constant.php 0000777 00000005242 15251156605 0014763 0 ustar 00 <?php /** * Class Base_Constant * * @package WooCommerce\Payments */ namespace WCPay\Constants; use ReflectionClass; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Base constant class to hold common logic for all constants. */ abstract class Base_Constant implements \JsonSerializable { /** * Enum value * * @var mixed */ protected $value; /** * Static objects cache. * * @var array */ protected static $object_cache = []; /** * Class constructor. Keep it private to only allow initializing from __callStatic() * * @param string $value Constant from class. * @throws \InvalidArgumentException */ final private function __construct( string $value ) { if ( $value instanceof static ) { $value = $value->get_value(); } elseif ( ! defined( static::class . "::$value" ) ) { throw new \InvalidArgumentException( "Constant with name '$value' does not exist." ); } $this->value = $value; } /** * Get enum class value. * * @return mixed */ public function get_value() { return $this->value; } /** * Compare to enums. * * @param mixed $variable Constant object to compare. * * @return bool */ final public function equals( $variable = null ): bool { return $this === $variable; } /** * Find constant in class by value. * * @param string $value Value to find. * * @return int|string * @throws \InvalidArgumentException */ public static function search( string $value ) { $class = new ReflectionClass( static::class ); $key = array_search( $value, $class->getConstants(), true ); if ( false === $key ) { throw new \InvalidArgumentException( "Constant with value '$value' does not exist." ); } return $key; } /** * Used to created enum from constant names like CLASS::ConstantName(). * * @param string $name Name of property or function. * @param array $arguments Arguments of static call. * * @return static * @throws \InvalidArgumentException */ public static function __callStatic( $name, $arguments ) { if ( ! isset( static::$object_cache[ $name ] ) ) { // Instantiating constants by class name using the 'new static($name)' approach is integral to this method's functionality. static::$object_cache[ $name ] = new static( $name ); } return static::$object_cache[ $name ]; } /** * Get real enum value. * * @return mixed|string */ public function __toString() { return constant( \get_class( $this ) . '::' . $this->get_value() ); } /** * Specify the value which should be serialized to JSON. * * @return mixed|string */ #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->__toString(); } } includes/constants/class-intent-status.php 0000777 00000002057 15251156605 0015045 0 ustar 00 <?php /** * Class Intent_Status * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * This class gives a list of all Payment and Setup Intent status name constants. * - https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status * - https://stripe.com/docs/api/setup_intents/object#setup_intent_object-status * * @phpstan-immutable */ class Intent_Status extends Base_Constant { const REQUIRES_PAYMENT_METHOD = 'requires_payment_method'; const REQUIRES_CONFIRMATION = 'requires_confirmation'; const REQUIRES_ACTION = 'requires_action'; const PROCESSING = 'processing'; const REQUIRES_CAPTURE = 'requires_capture'; const CANCELED = 'canceled'; const SUCCEEDED = 'succeeded'; /** * Stripe intents that are treated as successfully created, i.e. authorized. * * @type array */ const AUTHORIZED_STATUSES = [ self::SUCCEEDED, self::REQUIRES_CAPTURE, self::PROCESSING, ]; } includes/constants/class-track-events.php 0000777 00000001464 15251156605 0014632 0 ustar 00 <?php /** * Class Track_Events * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Event names for WooCommerce Analytics. * * @phpstan-immutable */ class Track_Events extends Base_Constant { // Payment method events. public const PAYMENT_METHOD_ENABLED = 'wcpay_payment_method_enabled'; public const PAYMENT_METHOD_DISABLED = 'wcpay_payment_method_disabled'; public const PAYMENT_METHOD_PROMOTION_DISMISSED = 'wcpay_payment_method_promotion_dismissed'; public const PAYMENT_METHOD_PROMOTION_ACTIVATED = 'wcpay_payment_method_promotion_activated'; public const PAYMENT_METHOD_PROMOTION_ACTIVATION_FAILED = 'wcpay_payment_method_promotion_activation_failed'; } includes/constants/class-order-mode.php 0000777 00000000577 15251156605 0014265 0 ustar 00 <?php /** * Class Order_Mode * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Enum saved in the order meta for the mode that the payment was processed in. * * @phpstan-immutable */ class Order_Mode extends Base_Constant { const TEST = 'test'; const PRODUCTION = 'prod'; } includes/constants/class-refund-status.php 0000777 00000000705 15251156605 0015025 0 ustar 00 <?php /** * Class Refund_Status * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * This class gives a list of all the possible WCPay refund status constants. * * @phpstan-immutable */ class Refund_Status extends Base_Constant { const PENDING = 'pending'; const SUCCEEDED = 'succeeded'; const FAILED = 'failed'; const CANCELED = 'canceled'; } includes/constants/class-fraud-meta-box-type.php 0000777 00000001430 15251156605 0016007 0 ustar 00 <?php /** * Class Fraud_Meta_Box_Type * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * This class gives a list of all the possible fraud meta box type constants. * * @phpstan-immutable */ class Fraud_Meta_Box_Type extends Base_Constant { const ALLOW = 'allow'; const BLOCK = 'block'; const NOT_CARD = 'not_card'; const NOT_WCPAY = 'not_wcpay'; const PAYMENT_STARTED = 'payment_started'; const REVIEW = 'review'; const REVIEW_ALLOWED = 'review_allowed'; const REVIEW_BLOCKED = 'review_blocked'; const REVIEW_EXPIRED = 'review_expired'; const REVIEW_FAILED = 'review_failed'; const TERMINAL_PAYMENT = 'terminal_payment'; } includes/constants/class-payment-intent-status.php 0000777 00000000577 15251156605 0016525 0 ustar 00 <?php /** * Class Payment_Intent_Status * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Payment_Intent_Status was deprecated in favor of Intent_Status. * * @deprecated 6.4.0 * @see Intent_Status * * @phpstan-immutable */ class Payment_Intent_Status extends Intent_Status { } includes/constants/class-payment-initiated-by.php 0000777 00000000726 15251156605 0016261 0 ustar 00 <?php /** * Class Payment_Information * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Enum used in WCPay\Payment_Information to determine whether a payment was * initated by a merchant or a customer. * * @phpstan-immutable */ class Payment_Initiated_By extends Base_Constant { const MERCHANT = 'initiated_by_merchant'; const CUSTOMER = 'initiated_by_customer'; } includes/constants/class-express-checkout-element-states.php 0000777 00000152611 15251156605 0020451 0 ustar 00 <?php /** * Class Express_Checkout_Element_States * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * This class provides a list of states for the Payment Request API to match against WooCommerce states. * The list is based on libaddressinput: https://github.com/google/libaddressinput, * which is used by Chromium based browsers in the native Payment Request address dialog. * * Known issues/inconsistencies: * 1. WC provides a dropdown list of states, but there's no state field in Chrome for the following countries: * AO (Angola), BD (Bangladesh), BG (Bulgaria), BJ (Benin), BO (Bolivia), DO (Dominican Republic), * DZ (Algeria), GH (Ghana), GT (Guatemala), HU (Hungary), KE (Kenya), LA (Laos), * LR (Liberia), MD (Moldova), NA (Namibia), NP (Nepal), PK (Pakistan), * PY (Paraguay), RO (Romania), TZ (Tanzania), UG (Uganda), UM (United States Minor Outlying Islands), * ZA (South Africa), ZM (Zambia). * 2. Chrome does not provide a dropdown list of states for 161 countries in total, out of the 249 countries WC supports, * so the countries in which the state field is required by WC, and not provided by the browser are not supported. * 3. Chrome does not provide a zip/postal code field for 60 out of the 249 countries WC supports. Only for 5 countries * the zip code field is missing while the state field is present: BS (Bahamas), PA (Panama), SC (Seychelles), * SR (Suriname), TV (Tuvalu). Several other countries provide an optional zip code field. * 4. WC expects it, but there's no city field in the Payment Request API for the following: * JP (Japan), MO (Macao), TN (Tunisia), KY (Cayman Islands), GI (Gibraltar). * 5. The following countries are not listed in WC: * AC (Ascension Island), IC (Canary Islands), EA (Ceuta & Melilla), CP (Clipperton Island), DG (Diego Garcia), * TA (Tristan da Cunha), XK (Kosovo). */ class Express_Checkout_Element_States { /** * A list of states which is compatible with Chromium based browsers for the Payment Request API. * If the input comes from Chrome, we will always match with `code`, but if the request comes from * Safari or other browsers which don't provide a dropdown list, we can match one of the following: * - [0] = code (string) * - [1] = name (string) * - [2] = localName (string|null) */ // phpcs:disable const STATES = [ // Afghanistan. Country_Code::AFGHANISTAN => [], // Angola. Country_Code::ANDORRA => [], // Argentina. Country_Code::ARGENTINA => [ 'C' => [ 'Ciudad Autónoma de Buenos Aires', 'Ciudad Autónoma de Buenos Aires', NULL ], 'B' => [ 'Buenos Aires', 'Buenos Aires', NULL ], 'K' => [ 'Catamarca', 'Catamarca', NULL ], 'H' => [ 'Chaco', 'Chaco', NULL ], 'U' => [ 'Chubut', 'Chubut', NULL ], 'X' => [ 'Córdoba', 'Córdoba', NULL ], 'W' => [ 'Corrientes', 'Corrientes', NULL ], 'E' => [ 'Entre Ríos', 'Entre Ríos', NULL ], 'P' => [ 'Formosa', 'Formosa', NULL ], 'Y' => [ 'Jujuy', 'Jujuy', NULL ], 'L' => [ 'La Pampa', 'La Pampa', NULL ], 'F' => [ 'La Rioja', 'La Rioja', NULL ], 'M' => [ 'Mendoza', 'Mendoza', NULL ], 'N' => [ 'Misiones', 'Misiones', NULL ], 'Q' => [ 'Neuquén', 'Neuquén', NULL ], 'R' => [ 'Río Negro', 'Río Negro', NULL ], 'A' => [ 'Salta', 'Salta', NULL ], 'J' => [ 'San Juan', 'San Juan', NULL ], 'D' => [ 'San Luis', 'San Luis', NULL ], 'Z' => [ 'Santa Cruz', 'Santa Cruz', NULL ], 'S' => [ 'Santa Fe', 'Santa Fe', NULL ], 'G' => [ 'Santiago del Estero', 'Santiago del Estero', NULL ], 'V' => [ 'Tierra del Fuego', 'Tierra del Fuego', NULL ], 'T' => [ 'Tucumán', 'Tucumán', NULL ], ], // Austria. Country_Code::AUSTRIA => [], // Australia. Country_Code::AUSTRALIA =>[ 'ACT' => [ 'ACT', 'Australian Capital Territory', NULL ], 'NSW' => [ 'NSW', 'New South Wales', NULL ], 'NT' => [ 'NT', 'Northern Territory', NULL ], 'QLD' => [ 'QLD', 'Queensland', NULL ], 'SA' => [ 'SA', 'South Australia', NULL ], 'TAS' => [ 'TAS', 'Tasmania', NULL ], 'VIC' => [ 'VIC', 'Victoria', NULL ], 'WA' => [ 'WA', 'Western Australia', NULL ], // [ 'JBT', 'Jervis Bay Territory', NULL ], ], // Aland Islands. 'AX' => [], // Bangladesh. Country_Code::BANGLADESH => [], // Belgium. Country_Code::BELGIUM => [], // Bulgaria. Country_Code::BULGARIA => [], // Bahrain. Country_Code::BAHRAIN => [], // Burundi. Country_Code::BURUNDI => [], // Benin. Country_Code::BENIN => [], // Bolivia. Country_Code::BOLIVIA => [], // Brazil. Country_Code::BRAZIL => [ 'AC' => [ 'AC', 'Acre', NULL ], 'AL' => [ 'AL', 'Alagoas', NULL ], 'AP' => [ 'AP', 'Amapá', NULL ], 'AM' => [ 'AM', 'Amazonas', NULL ], 'BA' => [ 'BA', 'Bahia', NULL ], 'CE' => [ 'CE', 'Ceará', NULL ], 'DF' => [ 'DF', 'Distrito Federal', NULL ], 'ES' => [ 'ES', 'Espírito Santo', NULL ], 'GO' => [ 'GO', 'Goiás', NULL ], 'MA' => [ 'MA', 'Maranhão', NULL ], 'MT' => [ 'MT', 'Mato Grosso', NULL ], 'MS' => [ 'MS', 'Mato Grosso do Sul', NULL ], 'MG' => [ 'MG', 'Minas Gerais', NULL ], 'PA' => [ 'PA', 'Pará', NULL ], 'PB' => [ 'PB', 'Paraíba', NULL ], 'PR' => [ 'PR', 'Paraná', NULL ], 'PE' => [ 'PE', 'Pernambuco', NULL ], 'PI' => [ 'PI', 'Piauí', NULL ], 'RJ' => [ 'RJ', 'Rio de Janeiro', NULL ], 'RN' => [ 'RN', 'Rio Grande do Norte', NULL ], 'RS' => [ 'RS', 'Rio Grande do Sul', NULL ], 'RO' => [ 'RO', 'Rondônia', NULL ], 'RR' => [ 'RR', 'Roraima', NULL ], 'SC' => [ 'SC', 'Santa Catarina', NULL ], 'SP' => [ 'SP', 'São Paulo', NULL ], 'SE' => [ 'SE', 'Sergipe', NULL ], 'TO' => [ 'TO', 'Tocantins', NULL ], ], // Canada. Country_Code::CANADA => [ 'AB' => [ 'AB', 'Alberta', 'Alberta' ], 'BC' => [ 'BC', 'British Columbia', 'Colombie-Britannique' ], 'MB' => [ 'MB', 'Manitoba', 'Manitoba' ], 'NB' => [ 'NB', 'New Brunswick', 'Nouveau-Brunswick' ], 'NL' => [ 'NL', 'Newfoundland and Labrador', 'Terre-Neuve-et-Labrador' ], 'NT' => [ 'NT', 'Northwest Territories', 'Territoires du Nord-Ouest' ], 'NS' => [ 'NS', 'Nova Scotia', 'Nouvelle-Écosse' ], 'NU' => [ 'NU', 'Nunavut', 'Nunavut' ], 'ON' => [ 'ON', 'Ontario', 'Ontario' ], 'PE' => [ 'PE', 'Prince Edward Island', 'Île-du-Prince-Édouard' ], 'QC' => [ 'QC', 'Quebec', 'Québec' ], 'SK' => [ 'SK', 'Saskatchewan', 'Saskatchewan' ], 'YT' => [ 'YT', 'Yukon', 'Yukon' ], ], // Switzerland. Country_Code::SWITZERLAND => [], // China. Country_Code::CHINA => [ 'CN1' => [ 'Yunnan Sheng', 'Yunnan Sheng', '云南省' ], 'CN2' => [ 'Beijing Shi', 'Beijing Shi', '北京市' ], 'CN3' => [ 'Tianjin Shi', 'Tianjin Shi', '天津市' ], 'CN4' => [ 'Hebei Sheng', 'Hebei Sheng', '河北省' ], 'CN5' => [ 'Shanxi Sheng', 'Shanxi Sheng', '山西省' ], 'CN6' => [ 'Neimenggu Zizhiqu', 'Neimenggu Zizhiqu', '内蒙古' ], 'CN7' => [ 'Liaoning Sheng', 'Liaoning Sheng', '辽宁省' ], 'CN8' => [ 'Jilin Sheng', 'Jilin Sheng', '吉林省' ], 'CN9' => [ 'Heilongjiang Sheng', 'Heilongjiang Sheng', '黑龙江省' ], 'CN10' => [ 'Shanghai Shi', 'Shanghai Shi', '上海市' ], 'CN11' => [ 'Jiangsu Sheng', 'Jiangsu Sheng', '江苏省' ], 'CN12' => [ 'Zhejiang Sheng', 'Zhejiang Sheng', '浙江省' ], 'CN13' => [ 'Anhui Sheng', 'Anhui Sheng', '安徽省' ], 'CN14' => [ 'Fujian Sheng', 'Fujian Sheng', '福建省' ], 'CN15' => [ 'Jiangxi Sheng', 'Jiangxi Sheng', '江西省' ], 'CN16' => [ 'Shandong Sheng', 'Shandong Sheng', '山东省' ], 'CN17' => [ 'Henan Sheng', 'Henan Sheng', '河南省' ], 'CN18' => [ 'Hubei Sheng', 'Hubei Sheng', '湖北省' ], 'CN19' => [ 'Hunan Sheng', 'Hunan Sheng', '湖南省' ], 'CN20' => [ 'Guangdong Sheng', 'Guangdong Sheng', '广东省' ], 'CN21' => [ 'Guangxi Zhuangzuzizhiqu', 'Guangxi Zhuangzuzizhiqu', '广西' ], 'CN22' => [ 'Hainan Sheng', 'Hainan Sheng', '海南省' ], 'CN23' => [ 'Chongqing Shi', 'Chongqing Shi', '重庆市' ], 'CN24' => [ 'Sichuan Sheng', 'Sichuan Sheng', '四川省' ], 'CN25' => [ 'Guizhou Sheng', 'Guizhou Sheng', '贵州省' ], 'CN26' => [ 'Shaanxi Sheng', 'Shaanxi Sheng', '陕西省' ], 'CN27' => [ 'Gansu Sheng', 'Gansu Sheng', '甘肃省' ], 'CN28' => [ 'Qinghai Sheng', 'Qinghai Sheng', '青海省' ], 'CN29' => [ 'Ningxia Huizuzizhiqu', 'Ningxia Huizuzizhiqu', '宁夏' ], 'CN30' => [ 'Macau', 'Macau', '澳门' ], 'CN31' => [ 'Xizang Zizhiqu', 'Xizang Zizhiqu', '西藏' ], 'CN32' => [ 'Xinjiang Weiwuerzizhiqu', 'Xinjiang Weiwuerzizhiqu', '新疆' ], // [ 'Taiwan', 'Taiwan', '台湾' ], // [ 'Hong Kong', 'Hong Kong', '香港' ], ], // Czech Republic. Country_Code::CZECHIA => [], // Germany. Country_Code::GERMANY => [], // Denmark. Country_Code::DENMARK => [], // Dominican Republic. Country_Code::DOMINICAN_REPUBLIC => [], // Algeria. Country_Code::ALGERIA => [], // Estonia. Country_Code::ESTONIA => [], // Egypt. Country_Code::EGYPT => [ 'EGALX' => [ 'Alexandria Governorate', 'Alexandria Governorate', 'الإسكندرية' ], 'EGASN' => [ 'Aswan Governorate', 'Aswan Governorate', 'أسوان' ], 'EGAST' => [ 'Asyut Governorate', 'Asyut Governorate', 'أسيوط' ], 'EGBA' => [ 'Red Sea Governorate', 'Red Sea Governorate', 'البحر الأحمر' ], 'EGBH' => [ 'El Beheira Governorate', 'El Beheira Governorate', 'البحيرة' ], 'EGBNS' => [ 'Beni Suef Governorate', 'Beni Suef Governorate', 'بني سويف' ], 'EGC' => [ 'Cairo Governorate', 'Cairo Governorate', 'القاهرة' ], 'EGDK' => [ 'Dakahlia Governorate', 'Dakahlia Governorate', 'الدقهلية' ], 'EGDT' => [ 'Damietta Governorate', 'Damietta Governorate', 'دمياط' ], 'EGFYM' => [ 'Faiyum Governorate', 'Faiyum Governorate', 'الفيوم' ], 'EGGH' => [ 'Gharbia Governorate', 'Gharbia Governorate', 'الغربية' ], 'EGGZ' => [ 'Giza Governorate', 'Giza Governorate', 'الجيزة' ], 'EGIS' => [ 'Ismailia Governorate', 'Ismailia Governorate', 'الإسماعيلية' ], 'EGJS' => [ 'South Sinai Governorate', 'South Sinai Governorate', 'جنوب سيناء' ], 'EGKB' => [ 'Qalyubia Governorate', 'Qalyubia Governorate', 'القليوبية' ], 'EGKFS' => [ 'Kafr El Sheikh Governorate', 'Kafr El Sheikh Governorate', 'كفر الشيخ' ], 'EGKN' => [ 'Qena Governorate', 'Qena Governorate', 'قنا' ], 'EGLX' => [ 'Luxor Governorate', 'Luxor Governorate', 'الأقصر' ], 'EGMN' => [ 'Menia Governorate', 'Menia Governorate', 'المنيا' ], 'EGMNF' => [ 'Menofia Governorate', 'Menofia Governorate', 'المنوفية' ], 'EGMT' => [ 'Matrouh Governorate', 'Matrouh Governorate', 'مطروح' ], 'EGPTS' => [ 'Port Said Governorate', 'Port Said Governorate', 'بورسعيد' ], 'EGSHG' => [ 'Sohag Governorate', 'Sohag Governorate', 'سوهاج' ], 'EGSHR' => [ 'Ash Sharqia Governorate', 'Ash Sharqia Governorate', 'الشرقية' ], 'EGSIN' => [ 'North Sinai Governorate', 'North Sinai Governorate', 'شمال سيناء' ], 'EGSUZ' => [ 'Suez Governorate', 'Suez Governorate', 'السويس' ], 'EGWAD' => [ 'New Valley Governorate', 'New Valley Governorate', 'الوادي الجديد' ], ], // Spain. Country_Code::SPAIN => [ 'C' => [ 'A Coruña', 'A Coruña', NULL ], 'VI' => [ 'Álava', 'Álava', NULL ], 'AB' => [ 'Albacete', 'Albacete', NULL ], 'A' => [ 'Alicante', 'Alicante', NULL ], 'AL' => [ 'Almería', 'Almería', NULL ], 'O' => [ 'Asturias', 'Asturias', NULL ], 'AV' => [ 'Ávila', 'Ávila', NULL ], 'BA' => [ 'Badajoz', 'Badajoz', NULL ], 'PM' => [ 'Balears', 'Balears', NULL ], 'B' => [ 'Barcelona', 'Barcelona', NULL ], 'BU' => [ 'Burgos', 'Burgos', NULL ], 'CC' => [ 'Cáceres', 'Cáceres', NULL ], 'CA' => [ 'Cádiz', 'Cádiz', NULL ], 'S' => [ 'Cantabria', 'Cantabria', NULL ], 'CS' => [ 'Castellón', 'Castellón', NULL ], 'CE' => [ 'Ceuta', 'Ceuta', NULL ], 'CR' => [ 'Ciudad Real', 'Ciudad Real', NULL ], 'CO' => [ 'Córdoba', 'Córdoba', NULL ], 'CU' => [ 'Cuenca', 'Cuenca', NULL ], 'GI' => [ 'Girona', 'Girona', NULL ], 'GR' => [ 'Granada', 'Granada', NULL ], 'GU' => [ 'Guadalajara', 'Guadalajara', NULL ], 'SS' => [ 'Guipúzcoa', 'Guipúzcoa', NULL ], 'H' => [ 'Huelva', 'Huelva', NULL ], 'HU' => [ 'Huesca', 'Huesca', NULL ], 'J' => [ 'Jaén', 'Jaén', NULL ], 'LO' => [ 'La Rioja', 'La Rioja', NULL ], 'GC' => [ 'Las Palmas', 'Las Palmas', NULL ], 'LE' => [ 'León', 'León', NULL ], 'L' => [ 'Lleida', 'Lleida', NULL ], 'LU' => [ 'Lugo', 'Lugo', NULL ], 'M' => [ 'Madrid', 'Madrid', NULL ], 'MA' => [ 'Málaga', 'Málaga', NULL ], 'ML' => [ 'Melilla', 'Melilla', NULL ], 'MU' => [ 'Murcia', 'Murcia', NULL ], 'NA' => [ 'Navarra', 'Navarra', NULL ], 'OR' => [ 'Ourense', 'Ourense', NULL ], 'P' => [ 'Palencia', 'Palencia', NULL ], 'PO' => [ 'Pontevedra', 'Pontevedra', NULL ], 'SA' => [ 'Salamanca', 'Salamanca', NULL ], 'TF' => [ 'Santa Cruz de Tenerife', 'Santa Cruz de Tenerife', NULL ], 'SG' => [ 'Segovia', 'Segovia', NULL ], 'SE' => [ 'Sevilla', 'Sevilla', NULL ], 'SO' => [ 'Soria', 'Soria', NULL ], 'T' => [ 'Tarragona', 'Tarragona', NULL ], 'TE' => [ 'Teruel', 'Teruel', NULL ], 'TO' => [ 'Toledo', 'Toledo', NULL ], 'V' => [ 'Valencia', 'Valencia', NULL ], 'VA' => [ 'Valladolid', 'Valladolid', NULL ], 'BI' => [ 'Vizcaya', 'Vizcaya', NULL ], 'ZA' => [ 'Zamora', 'Zamora', NULL ], 'Z' => [ 'Zaragoza', 'Zaragoza', NULL ], ], // Finland. Country_Code::FINLAND => [], // France. Country_Code::FRANCE => [], // French Guiana. 'GF' => [], // Ghana. Country_Code::GHANA => [], // Guadeloupe. 'GP' => [], // Greece. Country_Code::GREECE => [], // Guatemala. Country_Code::GUATEMALA => [], // Hong Kong. 'HK' => [ 'HONG KONG' => [ 'Hong Kong Island', 'Hong Kong Island', '香港島' ], 'KOWLOON' => [ 'Kowloon', 'Kowloon', '九龍' ], 'NEW TERRITORIES' => [ 'New Territories', 'New Territories', '新界' ], ], // Hungary. Country_Code::HUNGARY => [], // Indonesia. Country_Code::INDONESIA => [ 'AC' => [ 'Aceh', 'Aceh', NULL ], 'SU' => [ 'Sumatera Utara', 'Sumatera Utara', NULL ], 'SB' => [ 'Sumatera Barat', 'Sumatera Barat', NULL ], 'RI' => [ 'Riau', 'Riau', NULL ], 'KR' => [ 'Kepulauan Riau', 'Kepulauan Riau', NULL ], 'JA' => [ 'Jambi', 'Jambi', NULL ], 'SS' => [ 'Sumatera Selatan', 'Sumatera Selatan', NULL ], 'BB' => [ 'Kepulauan Bangka Belitung', 'Kepulauan Bangka Belitung', NULL ], 'BE' => [ 'Bengkulu', 'Bengkulu', NULL ], 'LA' => [ 'Lampung', 'Lampung', NULL ], 'JK' => [ 'DKI Jakarta', 'DKI Jakarta', NULL ], 'JB' => [ 'Jawa Barat', 'Jawa Barat', NULL ], 'BT' => [ 'Banten', 'Banten', NULL ], 'JT' => [ 'Jawa Tengah', 'Jawa Tengah', NULL ], 'JI' => [ 'Jawa Timur', 'Jawa Timur', NULL ], 'YO' => [ 'Daerah Istimewa Yogyakarta', 'Daerah Istimewa Yogyakarta', NULL ], 'BA' => [ 'Bali', 'Bali', NULL ], 'NB' => [ 'Nusa Tenggara Barat', 'Nusa Tenggara Barat', NULL ], 'NT' => [ 'Nusa Tenggara Timur', 'Nusa Tenggara Timur', NULL ], 'KB' => [ 'Kalimantan Barat', 'Kalimantan Barat', NULL ], 'KT' => [ 'Kalimantan Tengah', 'Kalimantan Tengah', NULL ], 'KI' => [ 'Kalimantan Timur', 'Kalimantan Timur', NULL ], 'KS' => [ 'Kalimantan Selatan', 'Kalimantan Selatan', NULL ], 'KU' => [ 'Kalimantan Utara', 'Kalimantan Utara', NULL ], 'SA' => [ 'Sulawesi Utara', 'Sulawesi Utara', NULL ], 'ST' => [ 'Sulawesi Tengah', 'Sulawesi Tengah', NULL ], 'SG' => [ 'Sulawesi Tenggara', 'Sulawesi Tenggara', NULL ], 'SR' => [ 'Sulawesi Barat', 'Sulawesi Barat', NULL ], 'SN' => [ 'Sulawesi Selatan', 'Sulawesi Selatan', NULL ], 'GO' => [ 'Gorontalo', 'Gorontalo', NULL ], 'MA' => [ 'Maluku', 'Maluku', NULL ], 'MU' => [ 'Maluku Utara', 'Maluku Utara', NULL ], 'PA' => [ 'Papua', 'Papua', NULL ], 'PB' => [ 'Papua Barat', 'Papua Barat', NULL ], // [ 'Kalimantan Tengah', 'Kalimantan Tengah', NULL ], // [ 'Kalimantan Timur', 'Kalimantan Timur', NULL ], ], // Ireland. Country_Code::IRELAND => [ 'CW' => [ 'Co. Carlow', 'Co. Carlow', NULL ], 'CN' => [ 'Co. Cavan', 'Co. Cavan', NULL ], 'CE' => [ 'Co. Clare', 'Co. Clare', NULL ], 'CO' => [ 'Co. Cork', 'Co. Cork', NULL ], 'DL' => [ 'Co. Donegal', 'Co. Donegal', NULL ], 'D' => [ 'Co. Dublin', 'Co. Dublin', NULL ], 'G' => [ 'Co. Galway', 'Co. Galway', NULL ], 'KY' => [ 'Co. Kerry', 'Co. Kerry', NULL ], 'KE' => [ 'Co. Kildare', 'Co. Kildare', NULL ], 'KK' => [ 'Co. Kilkenny', 'Co. Kilkenny', NULL ], 'LS' => [ 'Co. Laois', 'Co. Laois', NULL ], 'LM' => [ 'Co. Leitrim', 'Co. Leitrim', NULL ], 'LK' => [ 'Co. Limerick', 'Co. Limerick', NULL ], 'LD' => [ 'Co. Longford', 'Co. Longford', NULL ], 'LH' => [ 'Co. Louth', 'Co. Louth', NULL ], 'MO' => [ 'Co. Mayo', 'Co. Mayo', NULL ], 'MH' => [ 'Co. Meath', 'Co. Meath', NULL ], 'MN' => [ 'Co. Monaghan', 'Co. Monaghan', NULL ], 'OY' => [ 'Co. Offaly', 'Co. Offaly', NULL ], 'RN' => [ 'Co. Roscommon', 'Co. Roscommon', NULL ], 'SO' => [ 'Co. Sligo', 'Co. Sligo', NULL ], 'TA' => [ 'Co. Tipperary', 'Co. Tipperary', NULL ], 'WD' => [ 'Co. Waterford', 'Co. Waterford', NULL ], 'WH' => [ 'Co. Westmeath', 'Co. Westmeath', NULL ], 'WX' => [ 'Co. Wexford', 'Co. Wexford', NULL ], 'WW' => [ 'Co. Wicklow', 'Co. Wicklow', NULL ], ], // Israel. Country_Code::ISRAEL => [], // Isle of Man. 'IM' => [], // India. Country_Code::INDIA => [ 'AP' => [ 'Andhra Pradesh', 'Andhra Pradesh', NULL ], 'AR' => [ 'Arunachal Pradesh', 'Arunachal Pradesh', NULL ], 'AS' => [ 'Assam', 'Assam', NULL ], 'BR' => [ 'Bihar', 'Bihar', NULL ], 'CT' => [ 'Chhattisgarh', 'Chhattisgarh', NULL ], 'GA' => [ 'Goa', 'Goa', NULL ], 'GJ' => [ 'Gujarat', 'Gujarat', NULL ], 'HR' => [ 'Haryana', 'Haryana', NULL ], 'HP' => [ 'Himachal Pradesh', 'Himachal Pradesh', NULL ], 'JK' => [ 'Jammu and Kashmir', 'Jammu & Kashmir', NULL ], 'JH' => [ 'Jharkhand', 'Jharkhand', NULL ], 'KA' => [ 'Karnataka', 'Karnataka', NULL ], 'KL' => [ 'Kerala', 'Kerala', NULL ], // 'LA' => __( 'Ladakh', 'woocommerce' ), 'MP' => [ 'Madhya Pradesh', 'Madhya Pradesh', NULL ], 'MH' => [ 'Maharashtra', 'Maharashtra', NULL ], 'MN' => [ 'Manipur', 'Manipur', NULL ], 'ML' => [ 'Meghalaya', 'Meghalaya', NULL ], 'MZ' => [ 'Mizoram', 'Mizoram', NULL ], 'NL' => [ 'Nagaland', 'Nagaland', NULL ], 'OR' => [ 'Odisha', 'Odisha', NULL ], 'PB' => [ 'Punjab', 'Punjab', NULL ], 'RJ' => [ 'Rajasthan', 'Rajasthan', NULL ], 'SK' => [ 'Sikkim', 'Sikkim', NULL ], 'TN' => [ 'Tamil Nadu', 'Tamil Nadu', NULL ], 'TS' => [ 'Telangana', 'Telangana', NULL ], 'TR' => [ 'Tripura', 'Tripura', NULL ], 'UK' => [ 'Uttarakhand', 'Uttarakhand', NULL ], 'UP' => [ 'Uttar Pradesh', 'Uttar Pradesh', NULL ], 'WB' => [ 'West Bengal', 'West Bengal', NULL ], 'AN' => [ 'Andaman and Nicobar Islands', 'Andaman & Nicobar', NULL ], 'CH' => [ 'Chandigarh', 'Chandigarh', NULL ], 'DN' => [ 'Dadra and Nagar Haveli', 'Dadra & Nagar Haveli', NULL ], 'DD' => [ 'Daman and Diu', 'Daman & Diu', NULL ], 'DL' => [ 'Delhi', 'Delhi', NULL ], 'LD' => [ 'Lakshadweep', 'Lakshadweep', NULL ], 'PY' => [ 'Puducherry', 'Puducherry', NULL ], ], // Iran. Country_Code::IRAN => [ 'KHZ' => [ 'Khuzestan Province', 'Khuzestan Province', 'استان خوزستان' ], 'THR' => [ 'Tehran Province', 'Tehran Province', 'استان تهران' ], 'ILM' => [ 'Ilam Province', 'Ilam Province', 'استان ایلام' ], 'BHR' => [ 'Bushehr Province', 'Bushehr Province', 'استان بوشهر' ], 'ADL' => [ 'Ardabil Province', 'Ardabil Province', 'استان اردبیل' ], 'ESF' => [ 'Isfahan Province', 'Isfahan Province', 'استان اصفهان' ], 'YZD' => [ 'Yazd Province', 'Yazd Province', 'استان یزد' ], 'KRH' => [ 'Kermanshah Province', 'Kermanshah Province', 'استان کرمانشاه' ], 'KRN' => [ 'Kerman Province', 'Kerman Province', 'استان کرمان' ], 'HDN' => [ 'Hamadan Province', 'Hamadan Province', 'استان همدان' ], 'GZN' => [ 'Qazvin Province', 'Qazvin Province', 'استان قزوین' ], 'ZJN' => [ 'Zanjan Province', 'Zanjan Province', 'استان زنجان' ], 'LRS' => [ 'Lorestan Province', 'Lorestan Province', 'استان لرستان' ], 'ABZ' => [ 'Alborz Province', 'Alborz Province', 'استان البرز' ], 'EAZ' => [ 'East Azerbaijan Province', 'East Azerbaijan Province', 'استان آذربایجان شرقی' ], 'WAZ' => [ 'West Azerbaijan Province', 'West Azerbaijan Province', 'استان آذربایجان غربی' ], 'CHB' => [ 'Chaharmahal and Bakhtiari Province', 'Chaharmahal and Bakhtiari Province', 'استان چهارمحال و بختیاری' ], 'SKH' => [ 'South Khorasan Province', 'South Khorasan Province', 'استان خراسان جنوبی' ], 'RKH' => [ 'Razavi Khorasan Province', 'Razavi Khorasan Province', 'استان خراسان رضوی' ], 'NKH' => [ 'North Khorasan Province', 'North Khorasan Province', 'استان خراسان شمالی' ], 'SMN' => [ 'Semnan Province', 'Semnan Province', 'استان سمنان' ], 'FRS' => [ 'Fars Province', 'Fars Province', 'استان فارس' ], 'QHM' => [ 'Qom Province', 'Qom Province', 'استان قم' ], 'KRD' => [ 'Kurdistan Province', 'Kurdistan Province', 'استان کردستان' ], 'KBD' => [ 'Kohgiluyeh and Boyer-Ahmad Province', 'Kohgiluyeh and Boyer-Ahmad Province', 'استان کهگیلویه و بویراحمد' ], 'GLS' => [ 'Golestan Province', 'Golestan Province', 'استان گلستان' ], 'GIL' => [ 'Gilan Province', 'Gilan Province', 'استان گیلان' ], 'MZN' => [ 'Mazandaran Province', 'Mazandaran Province', 'استان مازندران' ], 'MKZ' => [ 'Markazi Province', 'Markazi Province', 'استان مرکزی' ], 'HRZ' => [ 'Hormozgan Province', 'Hormozgan Province', 'استان هرمزگان' ], 'SBN' => [ 'Sistan and Baluchestan Province', 'Sistan and Baluchestan Province', 'استان سیستان و بلوچستان' ], ], // Iceland. Country_Code::ICELAND => [], // Italy. Country_Code::ITALY => [ 'AG' => [ 'AG', 'Agrigento', NULL ], 'AL' => [ 'AL', 'Alessandria', NULL ], 'AN' => [ 'AN', 'Ancona', NULL ], 'AO' => [ 'AO', 'Aosta', NULL ], 'AR' => [ 'AR', 'Arezzo', NULL ], 'AP' => [ 'AP', 'Ascoli Piceno', NULL ], 'AT' => [ 'AT', 'Asti', NULL ], 'AV' => [ 'AV', 'Avellino', NULL ], 'BA' => [ 'BA', 'Bari', NULL ], 'BT' => [ 'BT', 'Barletta-Andria-Trani', NULL ], 'BL' => [ 'BL', 'Belluno', NULL ], 'BN' => [ 'BN', 'Benevento', NULL ], 'BG' => [ 'BG', 'Bergamo', NULL ], 'BI' => [ 'BI', 'Biella', NULL ], 'BO' => [ 'BO', 'Bologna', NULL ], 'BZ' => [ 'BZ', 'Bolzano', NULL ], 'BS' => [ 'BS', 'Brescia', NULL ], 'BR' => [ 'BR', 'Brindisi', NULL ], 'CA' => [ 'CA', 'Cagliari', NULL ], 'CL' => [ 'CL', 'Caltanissetta', NULL ], 'CB' => [ 'CB', 'Campobasso', NULL ], 'CE' => [ 'CE', 'Caserta', NULL ], 'CT' => [ 'CT', 'Catania', NULL ], 'CZ' => [ 'CZ', 'Catanzaro', NULL ], 'CH' => [ 'CH', 'Chieti', NULL ], 'CO' => [ 'CO', 'Como', NULL ], 'CS' => [ 'CS', 'Cosenza', NULL ], 'CR' => [ 'CR', 'Cremona', NULL ], 'KR' => [ 'KR', 'Crotone', NULL ], 'CN' => [ 'CN', 'Cuneo', NULL ], 'EN' => [ 'EN', 'Enna', NULL ], 'FM' => [ 'FM', 'Fermo', NULL ], 'FE' => [ 'FE', 'Ferrara', NULL ], 'FI' => [ 'FI', 'Firenze', NULL ], 'FG' => [ 'FG', 'Foggia', NULL ], 'FC' => [ 'FC', 'Forlì-Cesena', NULL ], 'FR' => [ 'FR', 'Frosinone', NULL ], 'GE' => [ 'GE', 'Genova', NULL ], 'GO' => [ 'GO', 'Gorizia', NULL ], 'GR' => [ 'GR', 'Grosseto', NULL ], 'IM' => [ 'IM', 'Imperia', NULL ], 'IS' => [ 'IS', 'Isernia', NULL ], 'SP' => [ 'SP', 'La Spezia', NULL ], 'AQ' => [ 'AQ', "L'Aquila", NULL ], 'LT' => [ 'LT', 'Latina', NULL ], 'LE' => [ 'LE', 'Lecce', NULL ], 'LC' => [ 'LC', 'Lecco', NULL ], 'LI' => [ 'LI', 'Livorno', NULL ], 'LO' => [ 'LO', 'Lodi', NULL ], 'LU' => [ 'LU', 'Lucca', NULL ], 'MC' => [ 'MC', 'Macerata', NULL ], 'MN' => [ 'MN', 'Mantova', NULL ], 'MS' => [ 'MS', 'Massa-Carrara', NULL ], 'MT' => [ 'MT', 'Matera', NULL ], 'ME' => [ 'ME', 'Messina', NULL ], 'MI' => [ 'MI', 'Milano', NULL ], 'MO' => [ 'MO', 'Modena', NULL ], 'MB' => [ 'MB', 'Monza e Brianza', NULL ], 'NA' => [ 'NA', 'Napoli', NULL ], 'NO' => [ 'NO', 'Novara', NULL ], 'NU' => [ 'NU', 'Nuoro', NULL ], 'OR' => [ 'OR', 'Oristano', NULL ], 'PD' => [ 'PD', 'Padova', NULL ], 'PA' => [ 'PA', 'Palermo', NULL ], 'PR' => [ 'PR', 'Parma', NULL ], 'PV' => [ 'PV', 'Pavia', NULL ], 'PG' => [ 'PG', 'Perugia', NULL ], 'PU' => [ 'PU', 'Pesaro e Urbino', NULL ], 'PE' => [ 'PE', 'Pescara', NULL ], 'PC' => [ 'PC', 'Piacenza', NULL ], 'PI' => [ 'PI', 'Pisa', NULL ], 'PT' => [ 'PT', 'Pistoia', NULL ], 'PN' => [ 'PN', 'Pordenone', NULL ], 'PZ' => [ 'PZ', 'Potenza', NULL ], 'PO' => [ 'PO', 'Prato', NULL ], 'RG' => [ 'RG', 'Ragusa', NULL ], 'RA' => [ 'RA', 'Ravenna', NULL ], 'RC' => [ 'RC', 'Reggio Calabria', NULL ], 'RE' => [ 'RE', 'Reggio Emilia', NULL ], 'RI' => [ 'RI', 'Rieti', NULL ], 'RN' => [ 'RN', 'Rimini', NULL ], 'RM' => [ 'RM', 'Roma', NULL ], 'RO' => [ 'RO', 'Rovigo', NULL ], 'SA' => [ 'SA', 'Salerno', NULL ], 'SS' => [ 'SS', 'Sassari', NULL ], 'SV' => [ 'SV', 'Savona', NULL ], 'SI' => [ 'SI', 'Siena', NULL ], 'SR' => [ 'SR', 'Siracusa', NULL ], 'SO' => [ 'SO', 'Sondrio', NULL ], 'SU' => [ 'SU', 'Sud Sardegna', NULL ], 'TA' => [ 'TA', 'Taranto', NULL ], 'TE' => [ 'TE', 'Teramo', NULL ], 'TR' => [ 'TR', 'Terni', NULL ], 'TO' => [ 'TO', 'Torino', NULL ], 'TP' => [ 'TP', 'Trapani', NULL ], 'TN' => [ 'TN', 'Trento', NULL ], 'TV' => [ 'TV', 'Treviso', NULL ], 'TS' => [ 'TS', 'Trieste', NULL ], 'UD' => [ 'UD', 'Udine', NULL ], 'VA' => [ 'VA', 'Varese', NULL ], 'VE' => [ 'VE', 'Venezia', NULL ], 'VB' => [ 'VB', 'Verbano-Cusio-Ossola', NULL ], 'VC' => [ 'VC', 'Vercelli', NULL ], 'VR' => [ 'VR', 'Verona', NULL ], 'VV' => [ 'VV', 'Vibo Valentia', NULL ], 'VI' => [ 'VI', 'Vicenza', NULL ], 'VT' => [ 'VT', 'Viterbo', NULL ], ], // Jamaica. Country_Code::JAMAICA => [ 'JM-01' => [ 'Kingston', 'Kingston', NULL ], 'JM-02' => [ 'St. Andrew', 'St. Andrew', NULL ], 'JM-03' => [ 'St. Thomas', 'St. Thomas', NULL ], 'JM-04' => [ 'Portland', 'Portland', NULL ], 'JM-05' => [ 'St. Mary', 'St. Mary', NULL ], 'JM-06' => [ 'St. Ann', 'St. Ann', NULL ], 'JM-07' => [ 'Trelawny', 'Trelawny', NULL ], 'JM-08' => [ 'St. James', 'St. James', NULL ], 'JM-09' => [ 'Hanover', 'Hanover', NULL ], 'JM-10' => [ 'Westmoreland', 'Westmoreland', NULL ], 'JM-11' => [ 'St. Elizabeth', 'St. Elizabeth', NULL ], 'JM-12' => [ 'Manchester', 'Manchester', NULL ], 'JM-13' => [ 'Clarendon', 'Clarendon', NULL ], 'JM-14' => [ 'St. Catherine', 'St. Catherine', NULL ], ], // Japan. Country_Code::JAPAN => [ 'JP01' => [ 'Hokkaido', 'Hokkaido', '北海道' ], 'JP02' => [ 'Aomori', 'Aomori', '青森県' ], 'JP03' => [ 'Iwate', 'Iwate', '岩手県' ], 'JP04' => [ 'Miyagi', 'Miyagi', '宮城県' ], 'JP05' => [ 'Akita', 'Akita', '秋田県' ], 'JP06' => [ 'Yamagata', 'Yamagata', '山形県' ], 'JP07' => [ 'Fukushima', 'Fukushima', '福島県' ], 'JP08' => [ 'Ibaraki', 'Ibaraki', '茨城県' ], 'JP09' => [ 'Tochigi', 'Tochigi', '栃木県' ], 'JP10' => [ 'Gunma', 'Gunma', '群馬県' ], 'JP11' => [ 'Saitama', 'Saitama', '埼玉県' ], 'JP12' => [ 'Chiba', 'Chiba', '千葉県' ], 'JP13' => [ 'Tokyo', 'Tokyo', '東京都' ], 'JP14' => [ 'Kanagawa', 'Kanagawa', '神奈川県' ], 'JP15' => [ 'Niigata', 'Niigata', '新潟県' ], 'JP16' => [ 'Toyama', 'Toyama', '富山県' ], 'JP17' => [ 'Ishikawa', 'Ishikawa', '石川県' ], 'JP18' => [ 'Fukui', 'Fukui', '福井県' ], 'JP19' => [ 'Yamanashi', 'Yamanashi', '山梨県' ], 'JP20' => [ 'Nagano', 'Nagano', '長野県' ], 'JP21' => [ 'Gifu', 'Gifu', '岐阜県' ], 'JP22' => [ 'Shizuoka', 'Shizuoka', '静岡県' ], 'JP23' => [ 'Aichi', 'Aichi', '愛知県' ], 'JP24' => [ 'Mie', 'Mie', '三重県' ], 'JP25' => [ 'Shiga', 'Shiga', '滋賀県' ], 'JP26' => [ 'Kyoto', 'Kyoto', '京都府' ], 'JP27' => [ 'Osaka', 'Osaka', '大阪府' ], 'JP28' => [ 'Hyogo', 'Hyogo', '兵庫県' ], 'JP29' => [ 'Nara', 'Nara', '奈良県' ], 'JP30' => [ 'Wakayama', 'Wakayama', '和歌山県' ], 'JP31' => [ 'Tottori', 'Tottori', '鳥取県' ], 'JP32' => [ 'Shimane', 'Shimane', '島根県' ], 'JP33' => [ 'Okayama', 'Okayama', '岡山県' ], 'JP34' => [ 'Hiroshima', 'Hiroshima', '広島県' ], 'JP35' => [ 'Yamaguchi', 'Yamaguchi', '山口県' ], 'JP36' => [ 'Tokushima', 'Tokushima', '徳島県' ], 'JP37' => [ 'Kagawa', 'Kagawa', '香川県' ], 'JP38' => [ 'Ehime', 'Ehime', '愛媛県' ], 'JP39' => [ 'Kochi', 'Kochi', '高知県' ], 'JP40' => [ 'Fukuoka', 'Fukuoka', '福岡県' ], 'JP41' => [ 'Saga', 'Saga', '佐賀県' ], 'JP42' => [ 'Nagasaki', 'Nagasaki', '長崎県' ], 'JP43' => [ 'Kumamoto', 'Kumamoto', '熊本県' ], 'JP44' => [ 'Oita', 'Oita', '大分県' ], 'JP45' => [ 'Miyazaki', 'Miyazaki', '宮崎県' ], 'JP46' => [ 'Kagoshima', 'Kagoshima', '鹿児島県' ], 'JP47' => [ 'Okinawa', 'Okinawa', '沖縄県' ], ], // Kenya. Country_Code::KENYA => [], // South Korea. Country_Code::SOUTH_KOREA => [], // Kuwait. Country_Code::KUWAIT => [], // Laos. Country_Code::LAOS => [], // Lebanon. Country_Code::LEBANON => [], // Sri Lanka. 'LK' => [], // Liberia. Country_Code::LIBERIA => [], // Luxembourg. Country_Code::LUXEMBOURG => [], // Moldova. Country_Code::MOLDOVA => [], // Martinique. 'MQ' => [], // Malta. Country_Code::MALTA => [], // Mexico. Country_Code::MEXICO => [ 'DF' => [ 'CDMX', 'Ciudad de México', NULL ], 'JA' => [ 'Jal.', 'Jalisco', NULL ], 'NL' => [ 'N.L.', 'Nuevo León', NULL ], 'AG' => [ 'Ags.', 'Aguascalientes', NULL ], 'BC' => [ 'B.C.', 'Baja California', NULL ], 'BS' => [ 'B.C.S.', 'Baja California Sur', NULL ], 'CM' => [ 'Camp.', 'Campeche', NULL ], 'CS' => [ 'Chis.', 'Chiapas', NULL ], 'CH' => [ 'Chih.', 'Chihuahua', NULL ], 'CO' => [ 'Coah.', 'Coahuila de Zaragoza', NULL ], 'CL' => [ 'Col.', 'Colima', NULL ], 'DG' => [ 'Dgo.', 'Durango', NULL ], 'GT' => [ 'Gto.', 'Guanajuato', NULL ], 'GR' => [ 'Gro.', 'Guerrero', NULL ], 'HG' => [ 'Hgo.', 'Hidalgo', NULL ], 'MX' => [ 'Méx.', 'Estado de México', NULL ], 'MI' => [ 'Mich.', 'Michoacán', NULL ], 'MO' => [ 'Mor.', 'Morelos', NULL ], 'NA' => [ 'Nay.', 'Nayarit', NULL ], 'OA' => [ 'Oax.', 'Oaxaca', NULL ], 'PU' => [ 'Pue.', 'Puebla', NULL ], 'QT' => [ 'Qro.', 'Querétaro', NULL ], 'QR' => [ 'Q.R.', 'Quintana Roo', NULL ], 'SL' => [ 'S.L.P.', 'San Luis Potosí', NULL ], 'SI' => [ 'Sin.', 'Sinaloa', NULL ], 'SO' => [ 'Son.', 'Sonora', NULL ], 'TB' => [ 'Tab.', 'Tabasco', NULL ], 'TM' => [ 'Tamps.', 'Tamaulipas', NULL ], 'TL' => [ 'Tlax.', 'Tlaxcala', NULL ], 'VE' => [ 'Ver.', 'Veracruz', NULL ], 'YU' => [ 'Yuc.', 'Yucatán', NULL ], 'ZA' => [ 'Zac.', 'Zacatecas', NULL ], ], // Malaysia. Country_Code::MALAYSIA => [ 'JHR' => [ 'Johor', 'Johor', NULL ], 'KDH' => [ 'Kedah', 'Kedah', NULL ], 'KTN' => [ 'Kelantan', 'Kelantan', NULL ], 'LBN' => [ 'Labuan', 'Labuan', NULL ], 'MLK' => [ 'Melaka', 'Melaka', NULL ], 'NSN' => [ 'Negeri Sembilan', 'Negeri Sembilan', NULL ], 'PHG' => [ 'Pahang', 'Pahang', NULL ], 'PNG' => [ 'Pulau Pinang', 'Pulau Pinang', NULL ], 'PRK' => [ 'Perak', 'Perak', NULL ], 'PLS' => [ 'Perlis', 'Perlis', NULL ], 'SBH' => [ 'Sabah', 'Sabah', NULL ], 'SWK' => [ 'Sarawak', 'Sarawak', NULL ], 'SGR' => [ 'Selangor', 'Selangor', NULL ], 'TRG' => [ 'Terengganu', 'Terengganu', NULL ], 'PJY' => [ 'Putrajaya', 'Putrajaya', NULL ], 'KUL' => [ 'Kuala Lumpur', 'Kuala Lumpur', NULL ], ], // Mozambique. Country_Code::MOZAMBIQUE => [ 'MZP' => [ 'Cabo Delgado', 'Cabo Delgado', NULL ], 'MZG' => [ 'Gaza', 'Gaza', NULL ], 'MZI' => [ 'Inhambane', 'Inhambane', NULL ], 'MZB' => [ 'Manica', 'Manica', NULL ], 'MZL' => [ 'Maputo', 'Maputo', NULL ], 'MZMPM' => [ 'Cidade de Maputo', 'Cidade de Maputo', NULL ], 'MZN' => [ 'Nampula', 'Nampula', NULL ], 'MZA' => [ 'Niassa', 'Niassa', NULL ], 'MZS' => [ 'Sofala', 'Sofala', NULL ], 'MZT' => [ 'Tete', 'Tete', NULL ], 'MZQ' => [ 'Zambezia', 'Zambezia', NULL ], ], // Namibia. Country_Code::NAMIBIA => [], // Nigeria. Country_Code::NIGERIA => [ 'AB' => [ 'Abia', 'Abia', NULL ], 'FC' => [ 'Federal Capital Territory', 'Federal Capital Territory', NULL ], 'AD' => [ 'Adamawa', 'Adamawa', NULL ], 'AK' => [ 'Akwa Ibom', 'Akwa Ibom', NULL ], 'AN' => [ 'Anambra', 'Anambra', NULL ], 'BA' => [ 'Bauchi', 'Bauchi', NULL ], 'BY' => [ 'Bayelsa', 'Bayelsa', NULL ], 'BE' => [ 'Benue', 'Benue', NULL ], 'BO' => [ 'Borno', 'Borno', NULL ], 'CR' => [ 'Cross River', 'Cross River', NULL ], 'DE' => [ 'Delta', 'Delta', NULL ], 'EB' => [ 'Ebonyi', 'Ebonyi', NULL ], 'ED' => [ 'Edo', 'Edo', NULL ], 'EK' => [ 'Ekiti', 'Ekiti', NULL ], 'EN' => [ 'Enugu', 'Enugu', NULL ], 'GO' => [ 'Gombe', 'Gombe', NULL ], 'IM' => [ 'Imo', 'Imo', NULL ], 'JI' => [ 'Jigawa', 'Jigawa', NULL ], 'KD' => [ 'Kaduna', 'Kaduna', NULL ], 'KN' => [ 'Kano', 'Kano', NULL ], 'KT' => [ 'Katsina', 'Katsina', NULL ], 'KE' => [ 'Kebbi', 'Kebbi', NULL ], 'KO' => [ 'Kogi', 'Kogi', NULL ], 'KW' => [ 'Kwara', 'Kwara', NULL ], 'LA' => [ 'Lagos', 'Lagos', NULL ], 'NA' => [ 'Nasarawa', 'Nasarawa', NULL ], 'NI' => [ 'Niger', 'Niger', NULL ], 'OG' => [ 'Ogun State', 'Ogun State', NULL ], 'ON' => [ 'Ondo', 'Ondo', NULL ], 'OS' => [ 'Osun', 'Osun', NULL ], 'OY' => [ 'Oyo', 'Oyo', NULL ], 'PL' => [ 'Plateau', 'Plateau', NULL ], 'RI' => [ 'Rivers', 'Rivers', NULL ], 'SO' => [ 'Sokoto', 'Sokoto', NULL ], 'TA' => [ 'Taraba', 'Taraba', NULL ], 'YO' => [ 'Yobe', 'Yobe', NULL ], 'ZA' => [ 'Zamfara', 'Zamfara', NULL ], ], // Netherlands. Country_Code::NETHERLANDS => [], // Norway. Country_Code::NORWAY => [], // Nepal. Country_Code::NEPAL => [], // New Zealand. Country_Code::NEW_ZEALAND => [], // Peru. Country_Code::PERU => [ 'CAL' => [ 'Callao', 'Callao', NULL ], 'LMA' => [ 'Municipalidad Metropolitana de Lima', 'Municipalidad Metropolitana de Lima', NULL ], 'AMA' => [ 'Amazonas', 'Amazonas', NULL ], 'ANC' => [ 'Áncash', 'Áncash', NULL ], 'APU' => [ 'Apurímac', 'Apurímac', NULL ], 'ARE' => [ 'Arequipa', 'Arequipa', NULL ], 'AYA' => [ 'Ayacucho', 'Ayacucho', NULL ], 'CAJ' => [ 'Cajamarca', 'Cajamarca', NULL ], 'CUS' => [ 'Cuzco', 'Cuzco', NULL ], 'HUV' => [ 'Huancavelica', 'Huancavelica', NULL ], 'HUC' => [ 'Huánuco', 'Huánuco', NULL ], 'ICA' => [ 'Ica', 'Ica', NULL ], 'JUN' => [ 'Junín', 'Junín', NULL ], 'LAL' => [ 'La Libertad', 'La Libertad', NULL ], 'LAM' => [ 'Lambayeque', 'Lambayeque', NULL ], 'LIM' => [ 'Gobierno Regional de Lima', 'Gobierno Regional de Lima', NULL ], 'LOR' => [ 'Loreto', 'Loreto', NULL ], 'MDD' => [ 'Madre de Dios', 'Madre de Dios', NULL ], 'MOQ' => [ 'Moquegua', 'Moquegua', NULL ], 'PAS' => [ 'Pasco', 'Pasco', NULL ], 'PIU' => [ 'Piura', 'Piura', NULL ], 'PUN' => [ 'Puno', 'Puno', NULL ], 'SAM' => [ 'San Martín', 'San Martín', NULL ], 'TAC' => [ 'Tacna', 'Tacna', NULL ], 'TUM' => [ 'Tumbes', 'Tumbes', NULL ], 'UCA' => [ 'Ucayali', 'Ucayali', NULL ], ], // Philippines. Country_Code::PHILIPPINES => [ 'ABR' => [ 'Abra', 'Abra', NULL ], 'AGN' => [ 'Agusan del Norte', 'Agusan del Norte', NULL ], 'AGS' => [ 'Agusan del Sur', 'Agusan del Sur', NULL ], 'AKL' => [ 'Aklan', 'Aklan', NULL ], 'ALB' => [ 'Albay', 'Albay', NULL ], 'ANT' => [ 'Antique', 'Antique', NULL ], 'APA' => [ 'Apayao', 'Apayao', NULL ], 'AUR' => [ 'Aurora', 'Aurora', NULL ], 'BAS' => [ 'Basilan', 'Basilan', NULL ], 'BAN' => [ 'Bataan', 'Bataan', NULL ], 'BTN' => [ 'Batanes', 'Batanes', NULL ], 'BTG' => [ 'Batangas', 'Batangas', NULL ], 'BEN' => [ 'Benguet', 'Benguet', NULL ], 'BIL' => [ 'Biliran', 'Biliran', NULL ], 'BOH' => [ 'Bohol', 'Bohol', NULL ], 'BUK' => [ 'Bukidnon', 'Bukidnon', NULL ], 'BUL' => [ 'Bulacan', 'Bulacan', NULL ], 'CAG' => [ 'Cagayan', 'Cagayan', NULL ], 'CAN' => [ 'Camarines Norte', 'Camarines Norte', NULL ], 'CAS' => [ 'Camarines Sur', 'Camarines Sur', NULL ], 'CAM' => [ 'Camiguin', 'Camiguin', NULL ], 'CAP' => [ 'Capiz', 'Capiz', NULL ], 'CAT' => [ 'Catanduanes', 'Catanduanes', NULL ], 'CAV' => [ 'Cavite', 'Cavite', NULL ], 'CEB' => [ 'Cebu', 'Cebu', NULL ], 'COM' => [ 'Compostela Valley', 'Compostela Valley', NULL ], 'NCO' => [ 'Cotabato', 'Cotabato', NULL ], 'DAV' => [ 'Davao del Norte', 'Davao del Norte', NULL ], 'DAS' => [ 'Davao del Sur', 'Davao del Sur', NULL ], 'DAC' => [ 'Davao Occidental', 'Davao Occidental', NULL ], 'DAO' => [ 'Davao Oriental', 'Davao Oriental', NULL ], 'DIN' => [ 'Dinagat Islands', 'Dinagat Islands', NULL ], 'EAS' => [ 'Eastern Samar', 'Eastern Samar', NULL ], 'GUI' => [ 'Guimaras', 'Guimaras', NULL ], 'IFU' => [ 'Ifugao', 'Ifugao', NULL ], 'ILN' => [ 'Ilocos Norte', 'Ilocos Norte', NULL ], 'ILS' => [ 'Ilocos Sur', 'Ilocos Sur', NULL ], 'ILI' => [ 'Iloilo', 'Iloilo', NULL ], 'ISA' => [ 'Isabela', 'Isabela', NULL ], 'KAL' => [ 'Kalinga', 'Kalinga', NULL ], 'LUN' => [ 'La Union', 'La Union', NULL ], 'LAG' => [ 'Laguna', 'Laguna', NULL ], 'LAN' => [ 'Lanao del Norte', 'Lanao del Norte', NULL ], 'LAS' => [ 'Lanao del Sur', 'Lanao del Sur', NULL ], 'LEY' => [ 'Leyte', 'Leyte', NULL ], 'MAG' => [ 'Maguindanao', 'Maguindanao', NULL ], 'MAD' => [ 'Marinduque', 'Marinduque', NULL ], 'MAS' => [ 'Masbate', 'Masbate', NULL ], 'MSC' => [ 'Misamis Occidental', 'Misamis Occidental', NULL ], 'MSR' => [ 'Misamis Oriental', 'Misamis Oriental', NULL ], 'MOU' => [ 'Mountain Province', 'Mountain Province', NULL ], 'NEC' => [ 'Negros Occidental', 'Negros Occidental', NULL ], 'NER' => [ 'Negros Oriental', 'Negros Oriental', NULL ], 'NSA' => [ 'Northern Samar', 'Northern Samar', NULL ], 'NUE' => [ 'Nueva Ecija', 'Nueva Ecija', NULL ], 'NUV' => [ 'Nueva Vizcaya', 'Nueva Vizcaya', NULL ], 'MDC' => [ 'Mindoro Occidental', 'Mindoro Occidental', NULL ], 'MDR' => [ 'Mindoro Oriental', 'Mindoro Oriental', NULL ], 'PLW' => [ 'Palawan', 'Palawan', NULL ], 'PAM' => [ 'Pampanga', 'Pampanga', NULL ], 'PAN' => [ 'Pangasinan', 'Pangasinan', NULL ], 'QUE' => [ 'Quezon Province', 'Quezon Province', NULL ], 'QUI' => [ 'Quirino', 'Quirino', NULL ], 'RIZ' => [ 'Rizal', 'Rizal', NULL ], 'ROM' => [ 'Romblon', 'Romblon', NULL ], 'WSA' => [ 'Samar', 'Samar', NULL ], 'SAR' => [ 'Sarangani', 'Sarangani', NULL ], 'SIQ' => [ 'Siquijor', 'Siquijor', NULL ], 'SOR' => [ 'Sorsogon', 'Sorsogon', NULL ], 'SCO' => [ 'South Cotabato', 'South Cotabato', NULL ], 'SLE' => [ 'Southern Leyte', 'Southern Leyte', NULL ], 'SUK' => [ 'Sultan Kudarat', 'Sultan Kudarat', NULL ], 'SLU' => [ 'Sulu', 'Sulu', NULL ], 'SUN' => [ 'Surigao del Norte', 'Surigao del Norte', NULL ], 'SUR' => [ 'Surigao del Sur', 'Surigao del Sur', NULL ], 'TAR' => [ 'Tarlac', 'Tarlac', NULL ], 'TAW' => [ 'Tawi-Tawi', 'Tawi-Tawi', NULL ], 'ZMB' => [ 'Zambales', 'Zambales', NULL ], 'ZAN' => [ 'Zamboanga del Norte', 'Zamboanga del Norte', NULL ], 'ZAS' => [ 'Zamboanga del Sur', 'Zamboanga del Sur', NULL ], 'ZSI' => [ 'Zamboanga Sibuguey', 'Zamboanga Sibuguey', NULL ], '00' => [ 'Metro Manila', 'Metro Manila', NULL ], ], // Pakistan. Country_Code::PAKISTAN => [], // Poland. Country_Code::POLAND => [], // Puerto Rico. 'PR' => [], // Portugal. Country_Code::PORTUGAL => [], // Paraguay. Country_Code::PARAGUAY => [], // Reunion. 'RE' => [], // Romania. Country_Code::ROMANIA => [], // Serbia. Country_Code::SERBIA => [], // Sweden. Country_Code::SWEDEN => [], // Singapore. Country_Code::SINGAPORE => [], // Slovenia. Country_Code::SLOVENIA => [], // Slovakia. Country_Code::SLOVAKIA => [], // Thailand. Country_Code::THAILAND => [ 'TH-37' => [ 'Amnat Charoen', 'Amnat Charoen', 'อำนาจเจริญ' ], 'TH-15' => [ 'Ang Thong', 'Ang Thong', 'อ่างทอง' ], 'TH-14' => [ 'Phra Nakhon Si Ayutthaya', 'Phra Nakhon Si Ayutthaya', 'พระนครศรีอยุธยา' ], 'TH-10' => [ 'Bangkok', 'Bangkok', 'กรุงเทพมหานคร' ], 'TH-38' => [ 'Bueng Kan', 'Bueng Kan', 'จังหวัด บึงกาฬ' ], 'TH-31' => [ 'Buri Ram', 'Buri Ram', 'บุรีรัมย์' ], 'TH-24' => [ 'Chachoengsao', 'Chachoengsao', 'ฉะเชิงเทรา' ], 'TH-18' => [ 'Chai Nat', 'Chai Nat', 'ชัยนาท' ], 'TH-36' => [ 'Chaiyaphum', 'Chaiyaphum', 'ชัยภูมิ' ], 'TH-22' => [ 'Chanthaburi', 'Chanthaburi', 'จันทบุรี' ], 'TH-50' => [ 'Chiang Rai', 'Chiang Rai', 'เชียงราย' ], 'TH-57' => [ 'Chiang Mai', 'Chiang Mai', 'เชียงใหม่' ], 'TH-20' => [ 'Chon Buri', 'Chon Buri', 'ชลบุรี' ], 'TH-86' => [ 'Chumpon', 'Chumpon', 'ชุมพร' ], 'TH-46' => [ 'Kalasin', 'Kalasin', 'กาฬสินธุ์' ], 'TH-62' => [ 'Kamphaeng Phet', 'Kamphaeng Phet', 'กำแพงเพชร' ], 'TH-71' => [ 'Kanchanaburi', 'Kanchanaburi', 'กาญจนบุรี' ], 'TH-40' => [ 'Khon Kaen', 'Khon Kaen', 'ขอนแก่น' ], 'TH-81' => [ 'Krabi', 'Krabi', 'กระบี่' ], 'TH-52' => [ 'Lampang', 'Lampang', 'ลำปาง' ], 'TH-51' => [ 'Lamphun', 'Lamphun', 'ลำพูน' ], 'TH-42' => [ 'Loei', 'Loei', 'เลย' ], 'TH-16' => [ 'Lop Buri', 'Lop Buri', 'ลพบุรี' ], 'TH-58' => [ 'Mae Hong Son', 'Mae Hong Son', 'แม่ฮ่องสอน' ], 'TH-44' => [ 'Maha Sarakham', 'Maha Sarakham', 'มหาสารคาม' ], 'TH-49' => [ 'Mukdahan', 'Mukdahan', 'มุกดาหาร' ], 'TH-26' => [ 'Nakhon Nayok', 'Nakhon Nayok', 'นครนายก' ], 'TH-73' => [ 'Nakhon Pathom', 'Nakhon Pathom', 'นครปฐม' ], 'TH-48' => [ 'Nakhon Phanom', 'Nakhon Phanom', 'นครพนม' ], 'TH-30' => [ 'Nakhon Ratchasima', 'Nakhon Ratchasima', 'นครราชสีมา' ], 'TH-60' => [ 'Nakhon Sawan', 'Nakhon Sawan', 'นครสวรรค์' ], 'TH-80' => [ 'Nakhon Si Thammarat', 'Nakhon Si Thammarat', 'นครศรีธรรมราช' ], 'TH-55' => [ 'Nan', 'Nan', 'น่าน' ], 'TH-96' => [ 'Narathiwat', 'Narathiwat', 'นราธิวาส' ], 'TH-39' => [ 'Nong Bua Lam Phu', 'Nong Bua Lam Phu', 'หนองบัวลำภู' ], 'TH-43' => [ 'Nong Khai', 'Nong Khai', 'หนองคาย' ], 'TH-12' => [ 'Nonthaburi', 'Nonthaburi', 'นนทบุรี' ], 'TH-13' => [ 'Pathum Thani', 'Pathum Thani', 'ปทุมธานี' ], 'TH-94' => [ 'Pattani', 'Pattani', 'ปัตตานี' ], 'TH-82' => [ 'Phang Nga', 'Phang Nga', 'พังงา' ], 'TH-93' => [ 'Phattalung', 'Phattalung', 'พัทลุง' ], 'TH-56' => [ 'Phayao', 'Phayao', 'พะเยา' ], 'TH-67' => [ 'Phetchabun', 'Phetchabun', 'เพชรบูรณ์' ], 'TH-76' => [ 'Phetchaburi', 'Phetchaburi', 'เพชรบุรี' ], 'TH-66' => [ 'Phichit', 'Phichit', 'พิจิตร' ], 'TH-65' => [ 'Phitsanulok', 'Phitsanulok', 'พิษณุโลก' ], 'TH-54' => [ 'Phrae', 'Phrae', 'แพร่' ], 'TH-83' => [ 'Phuket', 'Phuket', 'ภูเก็ต' ], 'TH-25' => [ 'Prachin Buri', 'Prachin Buri', 'ปราจีนบุรี' ], 'TH-77' => [ 'Prachuap Khiri Khan', 'Prachuap Khiri Khan', 'ประจวบคีรีขันธ์' ], 'TH-85' => [ 'Ranong', 'Ranong', 'ระนอง' ], 'TH-70' => [ 'Ratchaburi', 'Ratchaburi', 'ราชบุรี' ], 'TH-21' => [ 'Rayong', 'Rayong', 'ระยอง' ], 'TH-45' => [ 'Roi Et', 'Roi Et', 'ร้อยเอ็ด' ], 'TH-27' => [ 'Sa Kaeo', 'Sa Kaeo', 'สระแก้ว' ], 'TH-47' => [ 'Sakon Nakhon', 'Sakon Nakhon', 'สกลนคร' ], 'TH-11' => [ 'Samut Prakan', 'Samut Prakan', 'สมุทรปราการ' ], 'TH-74' => [ 'Samut Sakhon', 'Samut Sakhon', 'สมุทรสาคร' ], 'TH-75' => [ 'Samut Songkhram', 'Samut Songkhram', 'สมุทรสงคราม' ], 'TH-19' => [ 'Saraburi', 'Saraburi', 'สระบุรี' ], 'TH-91' => [ 'Satun', 'Satun', 'สตูล' ], 'TH-17' => [ 'Sing Buri', 'Sing Buri', 'สิงห์บุรี' ], 'TH-33' => [ 'Si Sa Ket', 'Si Sa Ket', 'ศรีสะเกษ' ], 'TH-90' => [ 'Songkhla', 'Songkhla', 'สงขลา' ], 'TH-64' => [ 'Sukhothai', 'Sukhothai', 'สุโขทัย' ], 'TH-72' => [ 'Suphanburi', 'Suphanburi', 'สุพรรณบุรี' ], 'TH-84' => [ 'Surat Thani', 'Surat Thani', 'สุราษฎร์ธานี' ], 'TH-32' => [ 'Surin', 'Surin', 'สุรินทร์' ], 'TH-63' => [ 'Tak', 'Tak', 'ตาก' ], 'TH-92' => [ 'Trang', 'Trang', 'ตรัง' ], 'TH-23' => [ 'Trat', 'Trat', 'ตราด' ], 'TH-34' => [ 'Ubon Ratchathani', 'Ubon Ratchathani', 'อุบลราชธานี' ], 'TH-41' => [ 'Udon Thani', 'Udon Thani', 'อุดรธานี' ], 'TH-61' => [ 'Uthai Thani', 'Uthai Thani', 'อุทัยธานี' ], 'TH-53' => [ 'Uttaradit', 'Uttaradit', 'อุตรดิตถ์' ], 'TH-95' => [ 'Yala', 'Yala', 'ยะลา' ], 'TH-35' => [ 'Yasothon', 'Yasothon', 'ยโสธร' ], ], // Turkey. Country_Code::TURKEY => [ 'TR01' => [ 'Adana', 'Adana', NULL ], 'TR02' => [ 'Adıyaman', 'Adıyaman', NULL ], 'TR03' => [ 'Afyon', 'Afyon', NULL ], 'TR04' => [ 'Ağrı', 'Ağrı', NULL ], 'TR05' => [ 'Amasya', 'Amasya', NULL ], 'TR06' => [ 'Ankara', 'Ankara', NULL ], 'TR07' => [ 'Antalya', 'Antalya', NULL ], 'TR08' => [ 'Artvin', 'Artvin', NULL ], 'TR09' => [ 'Aydın', 'Aydın', NULL ], 'TR10' => [ 'Balıkesir', 'Balıkesir', NULL ], 'TR11' => [ 'Bilecik', 'Bilecik', NULL ], 'TR12' => [ 'Bingöl', 'Bingöl', NULL ], 'TR13' => [ 'Bitlis', 'Bitlis', NULL ], 'TR14' => [ 'Bolu', 'Bolu', NULL ], 'TR15' => [ 'Burdur', 'Burdur', NULL ], 'TR16' => [ 'Bursa', 'Bursa', NULL ], 'TR17' => [ 'Çanakkale', 'Çanakkale', NULL ], 'TR18' => [ 'Çankırı', 'Çankırı', NULL ], 'TR19' => [ 'Çorum', 'Çorum', NULL ], 'TR20' => [ 'Denizli', 'Denizli', NULL ], 'TR21' => [ 'Diyarbakır', 'Diyarbakır', NULL ], 'TR22' => [ 'Edirne', 'Edirne', NULL ], 'TR23' => [ 'Elazığ', 'Elazığ', NULL ], 'TR24' => [ 'Erzincan', 'Erzincan', NULL ], 'TR25' => [ 'Erzurum', 'Erzurum', NULL ], 'TR26' => [ 'Eskişehir', 'Eskişehir', NULL ], 'TR27' => [ 'Gaziantep', 'Gaziantep', NULL ], 'TR28' => [ 'Giresun', 'Giresun', NULL ], 'TR29' => [ 'Gümüşhane', 'Gümüşhane', NULL ], 'TR30' => [ 'Hakkari', 'Hakkari', NULL ], 'TR31' => [ 'Hatay', 'Hatay', NULL ], 'TR32' => [ 'Isparta', 'Isparta', NULL ], 'TR33' => [ 'Mersin', 'Mersin', NULL ], 'TR34' => [ 'İstanbul', 'İstanbul', NULL ], 'TR35' => [ 'İzmir', 'İzmir', NULL ], 'TR36' => [ 'Kars', 'Kars', NULL ], 'TR37' => [ 'Kastamonu', 'Kastamonu', NULL ], 'TR38' => [ 'Kayseri', 'Kayseri', NULL ], 'TR39' => [ 'Kırklareli', 'Kırklareli', NULL ], 'TR40' => [ 'Kırşehir', 'Kırşehir', NULL ], 'TR41' => [ 'Kocaeli', 'Kocaeli', NULL ], 'TR42' => [ 'Konya', 'Konya', NULL ], 'TR43' => [ 'Kütahya', 'Kütahya', NULL ], 'TR44' => [ 'Malatya', 'Malatya', NULL ], 'TR45' => [ 'Manisa', 'Manisa', NULL ], 'TR46' => [ 'Kahramanmaraş', 'Kahramanmaraş', NULL ], 'TR47' => [ 'Mardin', 'Mardin', NULL ], 'TR48' => [ 'Muğla', 'Muğla', NULL ], 'TR49' => [ 'Muş', 'Muş', NULL ], 'TR50' => [ 'Nevşehir', 'Nevşehir', NULL ], 'TR51' => [ 'Niğde', 'Niğde', NULL ], 'TR52' => [ 'Ordu', 'Ordu', NULL ], 'TR53' => [ 'Rize', 'Rize', NULL ], 'TR54' => [ 'Sakarya', 'Sakarya', NULL ], 'TR55' => [ 'Samsun', 'Samsun', NULL ], 'TR56' => [ 'Siirt', 'Siirt', NULL ], 'TR57' => [ 'Sinop', 'Sinop', NULL ], 'TR58' => [ 'Sivas', 'Sivas', NULL ], 'TR59' => [ 'Tekirdağ', 'Tekirdağ', NULL ], 'TR60' => [ 'Tokat', 'Tokat', NULL ], 'TR61' => [ 'Trabzon', 'Trabzon', NULL ], 'TR62' => [ 'Tunceli', 'Tunceli', NULL ], 'TR63' => [ 'Şanlıurfa', 'Şanlıurfa', NULL ], 'TR64' => [ 'Uşak', 'Uşak', NULL ], 'TR65' => [ 'Van', 'Van', NULL ], 'TR66' => [ 'Yozgat', 'Yozgat', NULL ], 'TR67' => [ 'Zonguldak', 'Zonguldak', NULL ], 'TR68' => [ 'Aksaray', 'Aksaray', NULL ], 'TR69' => [ 'Bayburt', 'Bayburt', NULL ], 'TR70' => [ 'Karaman', 'Karaman', NULL ], 'TR71' => [ 'Kırıkkale', 'Kırıkkale', NULL ], 'TR72' => [ 'Batman', 'Batman', NULL ], 'TR73' => [ 'Şırnak', 'Şırnak', NULL ], 'TR74' => [ 'Bartın', 'Bartın', NULL ], 'TR75' => [ 'Ardahan', 'Ardahan', NULL ], 'TR76' => [ 'Iğdır', 'Iğdır', NULL ], 'TR77' => [ 'Yalova', 'Yalova', NULL ], 'TR78' => [ 'Karabük', 'Karabük', NULL ], 'TR79' => [ 'Kilis', 'Kilis', NULL ], 'TR80' => [ 'Osmaniye', 'Osmaniye', NULL ], 'TR81' => [ 'Düzce', 'Düzce', NULL ], ], // Tanzania. Country_Code::TANZANIA => [], // Uganda. Country_Code::UGANDA => [], // United States Minor Outlying Islands. 'UM' => [], // United States. Country_Code::UNITED_STATES => [ 'AL' => [ 'AL', 'Alabama', NULL ], 'AK' => [ 'AK', 'Alaska', NULL ], 'AZ' => [ 'AZ', 'Arizona', NULL ], 'AR' => [ 'AR', 'Arkansas', NULL ], 'CA' => [ 'CA', 'California', NULL ], 'CO' => [ 'CO', 'Colorado', NULL ], 'CT' => [ 'CT', 'Connecticut', NULL ], 'DE' => [ 'DE', 'Delaware', NULL ], 'DC' => [ 'DC', 'District of Columbia', NULL ], 'FL' => [ 'FL', 'Florida', NULL ], 'GA' => [ 'GA', 'Georgia', NULL ], 'HI' => [ 'HI', 'Hawaii', NULL ], 'ID' => [ 'ID', 'Idaho', NULL ], 'IL' => [ 'IL', 'Illinois', NULL ], 'IN' => [ 'IN', 'Indiana', NULL ], 'IA' => [ 'IA', 'Iowa', NULL ], 'KS' => [ 'KS', 'Kansas', NULL ], 'KY' => [ 'KY', 'Kentucky', NULL ], 'LA' => [ 'LA', 'Louisiana', NULL ], 'ME' => [ 'ME', 'Maine', NULL ], 'MD' => [ 'MD', 'Maryland', NULL ], 'MA' => [ 'MA', 'Massachusetts', NULL ], 'MI' => [ 'MI', 'Michigan', NULL ], 'MN' => [ 'MN', 'Minnesota', NULL ], 'MS' => [ 'MS', 'Mississippi', NULL ], 'MO' => [ 'MO', 'Missouri', NULL ], 'MT' => [ 'MT', 'Montana', NULL ], 'NE' => [ 'NE', 'Nebraska', NULL ], 'NV' => [ 'NV', 'Nevada', NULL ], 'NH' => [ 'NH', 'New Hampshire', NULL ], 'NJ' => [ 'NJ', 'New Jersey', NULL ], 'NM' => [ 'NM', 'New Mexico', NULL ], 'NY' => [ 'NY', 'New York', NULL ], 'NC' => [ 'NC', 'North Carolina', NULL ], 'ND' => [ 'ND', 'North Dakota', NULL ], 'OH' => [ 'OH', 'Ohio', NULL ], 'OK' => [ 'OK', 'Oklahoma', NULL ], 'OR' => [ 'OR', 'Oregon', NULL ], 'PA' => [ 'PA', 'Pennsylvania', NULL ], 'RI' => [ 'RI', 'Rhode Island', NULL ], 'SC' => [ 'SC', 'South Carolina', NULL ], 'SD' => [ 'SD', 'South Dakota', NULL ], 'TN' => [ 'TN', 'Tennessee', NULL ], 'TX' => [ 'TX', 'Texas', NULL ], 'UT' => [ 'UT', 'Utah', NULL ], 'VT' => [ 'VT', 'Vermont', NULL ], 'VA' => [ 'VA', 'Virginia', NULL ], 'WA' => [ 'WA', 'Washington', NULL ], 'WV' => [ 'WV', 'West Virginia', NULL ], 'WI' => [ 'WI', 'Wisconsin', NULL ], 'WY' => [ 'WY', 'Wyoming', NULL ], 'AA' => [ 'AA', 'Armed Forces (AA)', NULL ], 'AE' => [ 'AE', 'Armed Forces (AE)', NULL ], 'AP' => [ 'AP', 'Armed Forces (AP)', NULL ], //[ 'AS', 'American Samoa', NULL ], //[ 'GU', 'Guam', NULL ], //[ 'MH', 'Marshall Islands', NULL ], //[ 'FM', 'Micronesia', NULL ], //[ 'MP', 'Northern Mariana Islands', NULL ], //[ 'PW', 'Palau', NULL ], //[ 'PR', 'Puerto Rico', NULL ], //[ 'VI', 'Virgin Islands', NULL ], ], // Vietnam. Country_Code::VIETNAM => [], // Mayotte. 'YT' => [], // South Africa. Country_Code::SOUTH_AFRICA => [], // Zambia. Country_Code::ZAMBIA => [], ]; // phpcs:enable /** * List of countries where WooCommerce provides a state/region field, * but the Apple Pay and Google Pay address forms do not include a state/region field. * For these countries, we need to make the state field optional and hidden to avoid validation errors, * since the wallet-based checkout may not supply this information. * * We could not find an official list of countries that have this characteristic, * so we are basing this list on libaddressinput: https://github.com/google/libaddressinput, as a proxy. * The libaddressinput project is used by Chromium based browsers in the native address dialog. * * @see https://github.com/google/libaddressinput/wiki/AddressValidationMetadata */ const COUNTRIES_WITHOUT_STATES = [ Country_Code::ALGERIA, Country_Code::ANGOLA, Country_Code::BANGLADESH, Country_Code::BENIN, Country_Code::BOLIVIA, Country_Code::BULGARIA, Country_Code::CROATIA, Country_Code::DOMINICAN_REPUBLIC, Country_Code::GHANA, Country_Code::GUATEMALA, Country_Code::HUNGARY, Country_Code::KENYA, Country_Code::LAOS, Country_Code::LIBERIA, Country_Code::LITHUANIA, Country_Code::MOLDOVA, Country_Code::NAMIBIA, Country_Code::NEPAL, Country_Code::PAKISTAN, Country_Code::PARAGUAY, Country_Code::ROMANIA, Country_Code::SAUDI_ARABIA, Country_Code::SOUTH_AFRICA, Country_Code::TANZANIA, Country_Code::UGANDA, Country_Code::ZAMBIA, ]; } includes/constants/class-payment-method.php 0000777 00000003216 15251156605 0015154 0 ustar 00 <?php /** * Class Payment_Method * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Possible enum values for the type of the PaymentMethod. * https://stripe.com/docs/api/payment_methods/object#payment_method_object-type * * @phpstan-immutable */ class Payment_Method extends Base_Constant { /** * FLAG: PAYMENT_METHODS_LIST * We need to see how we can use the definitions to replace these constants. */ const ALIPAY = 'alipay'; const BANCONTACT = 'bancontact'; const BASC = 'bacs_debit'; const BECS = 'au_becs_debit'; const CARD = 'card'; const CARD_PRESENT = 'card_present'; const EPS = 'eps'; const GIROPAY = 'giropay'; const IDEAL = 'ideal'; const INTERAC_PRESENT = 'interac_present'; const LINK = 'link'; const P24 = 'p24'; const SEPA = 'sepa_debit'; const SOFORT = 'sofort'; const US_BANK_ACCOUNT = 'us_bank_account'; const AFFIRM = 'affirm'; const AFTERPAY = 'afterpay_clearpay'; const KLARNA = 'klarna'; const MULTIBANCO = 'multibanco'; const GRABPAY = 'grabpay'; const WECHAT_PAY = 'wechat_pay'; const GOOGLE_PAY = 'google_pay'; const APPLE_PAY = 'apple_pay'; const IPP_ALLOWED_PAYMENT_METHODS = [ self::CARD_PRESENT, self::INTERAC_PRESENT, ]; const BNPL_PAYMENT_METHODS = [ self::AFFIRM, self::AFTERPAY, self::KLARNA, ]; const OFFLINE_PAYMENT_METHODS = [ self::MULTIBANCO, ]; const REDIRECT_PAYMENT_METHODS = [ self::WECHAT_PAY, ]; } includes/constants/class-refund-failure-reason.php 0000777 00000004331 15251156605 0016415 0 ustar 00 <?php /** * Class Refund_Failure_Reason * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * This class gives a list of all the possible WCPay refund failure reason constants and their user-friendly messages. * Based on Stripe's refund failure reasons: https://docs.stripe.com/api/refunds/object#refund_object-failure_reason * * @phpstan-immutable */ class Refund_Failure_Reason extends Base_Constant { const LOST_OR_STOLEN_CARD = 'lost_or_stolen_card'; const EXPIRED_OR_CANCELED_CARD = 'expired_or_canceled_card'; const CHARGE_FOR_PENDING_REFUND_DISPUTED = 'charge_for_pending_refund_disputed'; const INSUFFICIENT_FUNDS = 'insufficient_funds'; const DECLINED = 'declined'; const MERCHANT_REQUEST = 'merchant_request'; const UNKNOWN = 'unknown'; /** * Get user-friendly message for a failure reason. * * @param string $failure_reason The failure reason code. * @return string The user-friendly message. */ public static function get_failure_message( string $failure_reason ): string { $messages = [ self::LOST_OR_STOLEN_CARD => __( 'The card used for the original payment has been reported lost or stolen.', 'woocommerce-payments' ), self::EXPIRED_OR_CANCELED_CARD => __( 'The card used for the original payment has expired or been canceled.', 'woocommerce-payments' ), self::CHARGE_FOR_PENDING_REFUND_DISPUTED => __( 'The charge for this refund is being disputed by the customer.', 'woocommerce-payments' ), self::INSUFFICIENT_FUNDS => __( 'Insufficient funds in your WooPayments balance.', 'woocommerce-payments' ), self::DECLINED => __( 'The refund was declined by the card issuer.', 'woocommerce-payments' ), self::MERCHANT_REQUEST => __( 'The refund was canceled at your request.', 'woocommerce-payments' ), self::UNKNOWN => __( 'An unknown error occurred while processing the refund.', 'woocommerce-payments' ), ]; return $messages[ $failure_reason ] ?? $messages[ self::UNKNOWN ]; } } includes/constants/class-country-code.php 0000777 00000024325 15251156605 0014640 0 ustar 00 <?php /** * Class Country_Code * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Country Code constants * * @phpstan-immutable */ class Country_Code extends Base_Constant { const AFGHANISTAN = 'AF'; const ALAND_ISLANDS = 'AX'; const ALBANIA = 'AL'; const ALGERIA = 'DZ'; const AMERICAN_SAMOA = 'AS'; const ANDORRA = 'AD'; const ANGOLA = 'AO'; const ANGUILLA = 'AI'; const ANTARCTICA = 'AQ'; const ANTIGUA_AND_BARBUDA = 'AG'; const ARGENTINA = 'AR'; const ARMENIA = 'AM'; const ARUBA = 'AW'; const AUSTRALIA = 'AU'; const AUSTRIA = 'AT'; const AZERBAIJAN = 'AZ'; const BAHAMAS = 'BS'; const BAHRAIN = 'BH'; const BANGLADESH = 'BD'; const BARBADOS = 'BB'; const BELARUS = 'BY'; const BELGIUM = 'BE'; const BELIZE = 'BZ'; const BENIN = 'BJ'; const BERMUDA = 'BM'; const BHUTAN = 'BT'; const BOLIVIA = 'BO'; const BOSNIA_AND_HERZEGOVINA = 'BA'; const BOTSWANA = 'BW'; const BOUVET_ISLAND = 'BV'; const BRAZIL = 'BR'; const BRITISH_INDIAN_OCEAN_TERRITORY = 'IO'; const BRUNEI = 'BN'; const BULGARIA = 'BG'; const BURKINA_FASO = 'BF'; const BURUNDI = 'BI'; const CABO_VERDE = 'CV'; const CAMBODIA = 'KH'; const CAMEROON = 'CM'; const CANADA = 'CA'; const CARIBBEAN_NETHERLANDS = 'BQ'; const CENTRAL_AFRICAN_REPUBLIC = 'CF'; const CHAD = 'TD'; const CHILE = 'CL'; const CHINA = 'CN'; const COCOS_KEELING_ISLANDS = 'CC'; const COLOMBIA = 'CO'; const COOK_ISLANDS = 'CK'; const COMOROS = 'KM'; const CONGO = 'CG'; const COSTA_RICA = 'CR'; const CROATIA = 'HR'; const CUBA = 'CU'; const CYPRUS = 'CY'; const CZECHIA = 'CZ'; const DEMOCRATIC_REPUBLIC_OF_THE_CONGO = 'CD'; const DENMARK = 'DK'; const DJIBOUTI = 'DJ'; const DOMINICA = 'DM'; const DOMINICAN_REPUBLIC = 'DO'; const EAST_TIMOR = 'TL'; const ECUADOR = 'EC'; const EGYPT = 'EG'; const EL_SALVADOR = 'SV'; const EQUATORIAL_GUINEA = 'GQ'; const ERITREA = 'ER'; const ESTONIA = 'EE'; const ESWATINI = 'SZ'; const ETHIOPIA = 'ET'; const FIJI = 'FJ'; const FINLAND = 'FI'; const FRANCE = 'FR'; const GABON = 'GA'; const GAMBIA = 'GM'; const GEORGIA = 'GE'; const GERMANY = 'DE'; const GIBRALTAR = 'GI'; const GHANA = 'GH'; const GREECE = 'GR'; const GRENADA = 'GD'; const GUATEMALA = 'GT'; const GUINEA = 'GN'; const GUINEA_BISSAU = 'GW'; const GUYANA = 'GY'; const HAITI = 'HT'; const HONDURAS = 'HN'; const HONG_KONG = 'HK'; const HUNGARY = 'HU'; const ICELAND = 'IS'; const INDIA = 'IN'; const INDONESIA = 'ID'; const IRAN = 'IR'; const IRAQ = 'IQ'; const IRELAND = 'IE'; const ISRAEL = 'IL'; const ITALY = 'IT'; const IVORY_COAST = 'CI'; const JAMAICA = 'JM'; const JAPAN = 'JP'; const JORDAN = 'JO'; const KAZAKHSTAN = 'KZ'; const KENYA = 'KE'; const KIRIBATI = 'KI'; const KOSOVO = 'XK'; const KUWAIT = 'KW'; const KYRGYZSTAN = 'KG'; const LAOS = 'LA'; const LATVIA = 'LV'; const LEBANON = 'LB'; const LESOTHO = 'LS'; const LIBERIA = 'LR'; const LIBYA = 'LY'; const LIECHTENSTEIN = 'LI'; const LITHUANIA = 'LT'; const LUXEMBOURG = 'LU'; const MADAGASCAR = 'MG'; const MALAWI = 'MW'; const MALAYSIA = 'MY'; const MALDIVES = 'MV'; const MALI = 'ML'; const MALTA = 'MT'; const MARSHALL_ISLANDS = 'MH'; const MAURITANIA = 'MR'; const MAURITIUS = 'MU'; const MEXICO = 'MX'; const MICRONESIA = 'FM'; const MOLDOVA = 'MD'; const MONACO = 'MC'; const MONGOLIA = 'MN'; const MONTENEGRO = 'ME'; const MOROCCO = 'MA'; const MOZAMBIQUE = 'MZ'; const MYANMAR = 'MM'; const NAMIBIA = 'NA'; const NAURU = 'NR'; const NEPAL = 'NP'; const NETHERLANDS = 'NL'; const NEW_ZEALAND = 'NZ'; const NICARAGUA = 'NI'; const NIGER = 'NE'; const NIGERIA = 'NG'; const NORTH_KOREA = 'KP'; const NORTH_MACEDONIA = 'MK'; const NORWAY = 'NO'; const OMAN = 'OM'; const PAKISTAN = 'PK'; const PALAU = 'PW'; const PALESTINE = 'PS'; const PANAMA = 'PA'; const PAPUA_NEW_GUINEA = 'PG'; const PARAGUAY = 'PY'; const PERU = 'PE'; const PHILIPPINES = 'PH'; const POLAND = 'PL'; const PORTUGAL = 'PT'; const PUERTO_RICO = 'PR'; const QATAR = 'QA'; const ROMANIA = 'RO'; const RUSSIA = 'RU'; const RWANDA = 'RW'; const SAINT_BARTHELEMY = 'BL'; const SAINT_KITTS_AND_NEVIS = 'KN'; const SAINT_LUCIA = 'LC'; const SAINT_VINCENT_AND_THE_GRENADINES = 'VC'; const SAMOA = 'WS'; const SAN_MARINO = 'SM'; const SAO_TOME_AND_PRINCIPE = 'ST'; const SAUDI_ARABIA = 'SA'; const SENEGAL = 'SN'; const SERBIA = 'RS'; const SEYCHELLES = 'SC'; const SIERRA_LEONE = 'SL'; const SINGAPORE = 'SG'; const SLOVAKIA = 'SK'; const SLOVENIA = 'SI'; const SOLOMON_ISLANDS = 'SB'; const SOMALIA = 'SO'; const SOUTH_AFRICA = 'ZA'; const SOUTH_KOREA = 'KR'; const SOUTH_SUDAN = 'SS'; const SPAIN = 'ES'; const SRI_LANKA = 'LK'; const SUDAN = 'SD'; const SURINAME = 'SR'; const SWEDEN = 'SE'; const SWITZERLAND = 'CH'; const SYRIA = 'SY'; const TAIWAN = 'TW'; const TAJIKISTAN = 'TJ'; const TANZANIA = 'TZ'; const THAILAND = 'TH'; const TOGO = 'TG'; const TONGA = 'TO'; const TRINIDAD_AND_TOBAGO = 'TT'; const TUNISIA = 'TN'; const TURKEY = 'TR'; const TURKMENISTAN = 'TM'; const TUVALU = 'TV'; const UGANDA = 'UG'; const UKRAINE = 'UA'; const UNITED_ARAB_EMIRATES = 'AE'; const UNITED_KINGDOM = 'GB'; const UNITED_STATES = 'US'; const URUGUAY = 'UY'; const UZBEKISTAN = 'UZ'; const VANUATU = 'VU'; const VATICAN_CITY = 'VA'; const VENEZUELA = 'VE'; const VIETNAM = 'VN'; const YEMEN = 'YE'; const ZAMBIA = 'ZM'; const ZIMBABWE = 'ZW'; } includes/constants/class-payment-capture-type.php 0000777 00000000706 15251156605 0016317 0 ustar 00 <?php /** * Class Capture_Type * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Enum used in WCPay\Payment_Information to determine whether a payment was * initated by a merchant or a customer. * * @phpstan-immutable */ class Payment_Capture_Type extends Base_Constant { const AUTOMATIC = 'automatic_capture'; const MANUAL = 'manual_capture'; } includes/constants/class-express-checkout-hong-kong-states.php 0000777 00000012666 15251156605 0020714 0 ustar 00 <?php /** * Class Express_Checkout_Hong_Kong_States * * @package WooCommerce\Payments */ namespace WCPay\Constants; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Class Express_Checkout_Hong_Kong_States * * Contains a list of districts (equivalent to WC states) in Hong Kong used for normalization and validation purposes. * This is necessary due to a bug in Apple Pay that's currently being worked on. Until * that bug is fixed this workaround will be necessary. * * More info in pc4etw-bY-p2. * * Note: The following code has been kindly sourced from the WooCommerce Stripe Payment Gateway implementation, * which addresses the same issue in that plugin. See https://github.com/woocommerce/woocommerce-gateway-stripe/pull/1593. * * @since x.x.x */ class Express_Checkout_Hong_Kong_States { // Source: https://www.rvd.gov.hk/doc/tc/hkpr13/06.pdf. const STATES = [ 'hong kong', 'hong kong island', '港島', 'central and western', '中西區', 'kennedy town', 'shek tong tsui', 'sai ying pun', 'sheung wan', 'central', 'admiralty', 'mid-levels', 'peak', '堅尼地城', '石塘咀', '西營盤', '上環', '中環', '金鐘', '半山區', '山頂', 'wan chai', '灣仔', 'causeway bay', 'happy valley', 'tai hang', 'so kon po', "jardine's lookout", '灣仔', '銅鑼灣', '跑馬地', '大坑', '掃桿埔', '渣甸山', 'eastern', '東區', 'tin hau', 'braemar hill', 'north point', 'quarry bay', 'sai wan ho', 'shau kei wan', 'chai wan', 'siu sai wan', '天后', '寶馬山', '北角', '鰂魚涌', '西灣河', '筲箕灣', '柴灣', '小西灣', 'southern', '南區', 'pok fu lam', 'aberdeen', 'ap lei chau', 'wong chuk hang', 'shouson hill', 'repulse bay', 'chung hom kok', 'stanley', 'tai tam', 'shek o', '薄扶林', '香港仔', '鴨脷洲', '黃竹坑', '壽臣山', '淺水灣', '舂磡角', '赤柱', '大潭', '石澳', 'kowloon', '九龍', 'yau tsim mong', '油尖旺', 'tsim sha tsui', 'yau ma tei', 'west kowloon reclamation', "king's park, mong kok", 'tai kok tsui', '尖沙咀', '油麻地', '西九龍填海區', '京士柏', '旺角', '大角咀', 'sham shui po', '深水埗', 'mei foo', 'lai chi kok', 'cheung sha wan', 'shek kip mei', 'yau yat tsuen', 'tai wo ping', 'stonecutters island', '美孚', '荔枝角', '長沙灣', '石硤尾', '又一村', '大窩坪', '昂船洲', 'kowloon city', '九龍城', 'hung hom', 'to kwa wan', 'ma tau kok', 'ma tau wai', 'kai tak', 'ho man tin', 'kowloon tong', 'beacon hill', '紅磡', '土瓜灣', '馬頭角', '馬頭圍', '啟德', '何文田', '九龍塘', '筆架山', 'wong tai sin', '黃大仙', 'san po kong', 'tung tau', 'wang tau hom', 'lok fu', 'diamond hill', 'tsz wan shan', 'ngau chi wan', '新蒲崗', '東頭', '橫頭磡', '樂富', '鑽石山', '慈雲山', '牛池灣', 'kwun tong', '觀塘', 'ping shek', 'kowloon bay', 'ngau tau kok', 'jordan valley', 'kwun tong', 'sau mau ping', 'lam tin', 'yau tong', 'lei yue mun', '坪石', '九龍灣', '牛頭角', '佐敦谷', '觀塘', '秀茂坪', '藍田', '油塘', '鯉魚門', 'new territories', '新界', 'kwai tsing', '葵青', 'kwai chung', 'tsing yi', '葵涌', '青衣', 'tsuen wan', '荃灣', 'lei muk shue', 'ting kau', 'sham tseng', 'tsing lung tau', 'ma wan', 'sunny bay', '梨木樹', '汀九', '深井', '青龍頭', '馬灣', '欣澳', 'tuen mun', '屯門', 'tai lam chung', 'so kwun wat', 'tuen mun', 'lam tei', '大欖涌', '掃管笏', '屯門', '藍地', 'yuen long', '元朗', 'hung shui kiu', 'ha tsuen', 'lau fau shan', 'tin shui wai', 'yuen long', 'san tin', 'lok ma chau', 'kam tin', 'shek kong', 'pat heung', '洪水橋', '廈村', '流浮山', '天水圍', '元朗', '新田', '落馬洲', '錦田', '石崗', '八鄉', 'north', '北區', 'fanling', 'luen wo hui', 'sheung shui', 'shek wu hui', 'sha tau kok', 'luk keng', 'wu kau tang', '粉嶺', '聯和墟', '上水', '石湖墟', '沙頭角', '鹿頸', '烏蛟騰', 'tai po', '大埔', 'tai po market', 'tai po kau', 'tai mei tuk', 'shuen wan', 'cheung muk tau', 'kei ling ha', '大埔墟', '大埔', '大埔滘', '大尾篤', '船灣', '樟木頭', '企嶺下', 'sha tin', '沙田', 'tai wai', 'fo tan', 'ma liu shui', 'wu kai sha', 'ma on shan', '大圍', '火炭', '馬料水', '烏溪沙', '馬鞍山', 'sai kung', '西貢', 'clear water bay', 'tai mong tsai', 'tseung kwan o', 'hang hau', 'tiu keng leng', 'ma yau tong', '清水灣', '大網仔', '將軍澳', '坑口', '調景嶺', '馬游塘', 'islands', '離島', 'cheung chau', 'peng chau', 'lantau island (including tung chung)', 'lamma island', '長洲', '坪洲', '大嶼山(包括東涌)', '南丫島', ]; /** * Checks if the given state is a valid region (equivalent to WC state) in Hong Kong. * * @param string $state The state to be evaluated. * @return bool True if the provided state is valid, false otherwise. */ public static function is_valid_state( $state ) { return in_array( $state, self::STATES, true ); } } includes/class-wc-payments.php 0000777 00000262553 15251156605 0012467 0 ustar 00 <?php /** * Class WC_Payments * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Core\Mode; use WCPay\Core\Server\Request; use WCPay\Migrations\Allowed_Payment_Request_Button_Types_Update; use WCPay\Payment_Methods\CC_Payment_Method; use WCPay\Payment_Methods\UPE_Payment_Method; use WCPay\PaymentMethods\Configs\Definitions\GiropayDefinition; use WCPay\PaymentMethods\Configs\Definitions\SofortDefinition; use WCPay\WooPay_Tracker; use WCPay\WooPay\WooPay_Utilities; use WCPay\WooPay\WooPay_Order_Status_Sync; use WCPay\Session_Rate_Limiter; use WCPay\Database_Cache; use WCPay\WC_Payments_Checkout; use WCPay\WooPay\Service\Checkout_Service; use WCPay\Core\WC_Payments_Customer_Service_API; use WCPay\Constants\Payment_Method; use WCPay\Duplicate_Payment_Prevention_Service; use WCPay\Internal\Service\Level3Service; use WCPay\Internal\Service\OrderService; use WCPay\WooPay\WooPay_Scheduler; use WCPay\WooPay\WooPay_Session; use WCPay\Compatibility_Service; use WCPay\Duplicates_Detection_Service; use WCPay\WC_Payments_Currency_Manager; use WCPay\PaymentMethods\Configs\Registry\PaymentMethodDefinitionRegistry; /** * Main class for the WooPayments extension. Its responsibility is to initialize the extension. */ class WC_Payments { /** * Main payment gateway controller instance, created in init function. * * @var WC_Payment_Gateway_WCPay */ private static $card_gateway; /** * Instance of WC_Payments_API_Client, created in init function. * * @var WC_Payments_API_Client */ private static $api_client; /** * Instance of WC_Payments_DB. * * @var WC_Payments_DB */ private static $db_helper; /** * Instance of WC_Payments_Account, created in init function. * * @var WC_Payments_Account */ private static $account; /** * Instance of WC_Payments_Session_Service, created in init function. * * @var WC_Payments_Session_Service */ private static $session_service; /** * Instance of WC_Payments_Redirect_Service, created in init function. * * @var WC_Payments_Redirect_Service */ private static $redirect_service; /** * Instance of WC_Payments_Customer_Service, created in init function. * * @var WC_Payments_Customer_Service */ private static $customer_service; /** * Instance of WC_Payments_Token_Service, created in init function. * * @var WC_Payments_Token_Service */ private static $token_service; /** * Instance of WC_Payments_Remote_Note_Service, created in init function. * * @var WC_Payments_Remote_Note_Service */ private static $remote_note_service; /** * Instance of WC_Payments_Action_Scheduler_Service, created in init function * * @var WC_Payments_Action_Scheduler_Service */ private static $action_scheduler_service; /** * Instance of WC_Payments_Localization_Service, created in init function * * @var WC_Payments_Localization_Service */ private static $localization_service; /** * Instance of WC_Payments_Settings_Service, created in init function * * @var WC_Payments_Settings_Service */ private static $settings_service; /** * Instance of WC_Payments_Dependency_Service, created in init function * * @var WC_Payments_Dependency_Service */ private static $dependency_service; /** * Instance of WC_Payments_Fraud_Service, created in init function * * @var WC_Payments_Fraud_Service */ private static $fraud_service; /** * Instance of WC_Payments_In_Person_Payments_Receipts_Service, created in init function * * @var WC_Payments_In_Person_Payments_Receipts_Service */ private static $in_person_payments_receipts_service; /** * Instance of WC_Payments_Order_Service, created in init function * * @var WC_Payments_Order_Service */ private static $order_service; /** * Instance of WC_Payments_Order_Success_Page, created in init function * * @var WC_Payments_Order_Success_Page */ private static $order_success_page; /** * Instance of WC_Payments_Onboarding_Service, created in init function * * @var WC_Payments_Onboarding_Service */ private static $onboarding_service; /** * Instance of WC_Payments_Apple_Pay_Registration, created in init function * * @var WC_Payments_Apple_Pay_Registration */ private static $apple_pay_registration; /** * Instance of Session_Rate_Limiter to limit failed transactions * * @var Session_Rate_Limiter */ private static $failed_transaction_rate_limiter; /** * Instance of Database_Cache utils * * @var Database_Cache */ private static $database_cache; /** * Cache for plugin headers to avoid multiple calls to get_file_data * * @var array */ private static $plugin_headers = null; /** * Instance of WC_Payments_Webhook_Processing_Service to process webhook data. * * @var WC_Payments_Webhook_Processing_Service */ private static $webhook_processing_service; /** * Maps all availabled Stripe payment method IDs to Payment Method instances. * * @var array */ private static $payment_method_map = []; /** * Maps all availabled Stripe payment method IDs to Payment Gateway instances. * * @var array */ private static $payment_gateway_map = []; /** * Instance of WC_Payments_Webhook_Reliability_Service, created in init function * * @var WC_Payments_Webhook_Reliability_Service */ private static $webhook_reliability_service; /** * Holds WCPay's working mode. * * @var Mode */ private static $mode; /** * WooPay Utilities. * * @var WooPay_Utilities */ private static $woopay_util; /** * WooPay Tracker. * * @var WooPay_Tracker */ private static $woopay_tracker; /** * WC Payments Checkout * * @var WC_Payments_Checkout */ private static $wc_payments_checkout; /** * WooPay Checkout service * * @var Checkout_Service */ private static $woopay_checkout_service; /** * WC Payments Customer Service API * * @var WC_Payments_Customer_Service_API */ private static $customer_service_api; /** * Duplicate payment prevention service. * * @var Duplicate_Payment_Prevention_Service */ private static $duplicate_payment_prevention_service; /** * Instance of WC_Payments_Incentives_Service, created in init function. * * @var WC_Payments_Incentives_Service */ private static $incentives_service; /** * Instance of WC_Payments_PM_Promotions_Service, created in init function. * * @var WC_Payments_PM_Promotions_Service */ private static $pm_promotions_service; /** * Instance of WC_Payments_Express_Checkout_Button_Helper, created in init function. * * @var WC_Payments_Express_Checkout_Button_Helper */ private static $express_checkout_helper; /** * Instance of Compatibility_Service, created in init function * * @var Compatibility_Service */ private static $compatibility_service; /** * Instance of Duplicates_Detection_Service, created in init function * * @var Duplicates_Detection_Service */ private static $duplicates_detection_service; /** * Instance of WC_Payments_Currency_Manager, created in init function * * @var WC_Payments_Currency_Manager */ private static $currency_manager; /** * Instance of WC_Payments_Payment_Method_Service, created in init function * * @var WC_Payments_Payment_Method_Service */ private static $payment_method_service; /** * Instance of WC_Payments_Remediate_Canceled_Auth_Fees, created in init function * * @var WC_Payments_Remediate_Canceled_Auth_Fees */ private static $fee_remediation; /** * Entry point to the initialization logic. */ public static function init() { // in TeamCity tests, there might be multiple instances of WC_Payments initialized. // This prevents the hooks from being registered multiple times, which can cause issues. if ( defined( 'WCPAY_VERSION_NUMBER' ) ) { return; } define( 'WCPAY_VERSION_NUMBER', self::get_plugin_headers()['Version'] ); include_once __DIR__ . '/class-wc-payments-utils.php'; include_once __DIR__ . '/core/class-mode.php'; self::$mode = new Mode(); include_once __DIR__ . '/class-database-cache.php'; self::$database_cache = new Database_Cache(); self::$database_cache->init_hooks(); include_once __DIR__ . '/class-wc-payments-dependency-service.php'; self::$dependency_service = new WC_Payments_Dependency_Service(); self::$dependency_service->init_hooks(); if ( false === self::$dependency_service->has_valid_dependencies() ) { return; } add_action( 'admin_init', [ __CLASS__, 'add_woo_admin_notes' ] ); add_action( 'admin_init', [ __CLASS__, 'remove_deprecated_notes' ] ); add_action( 'init', [ __CLASS__, 'install_actions' ] ); add_action( 'woocommerce_blocks_payment_method_type_registration', [ __CLASS__, 'register_checkout_gateway' ] ); include_once __DIR__ . '/class-wc-payments-db.php'; self::$db_helper = new WC_Payments_DB(); include_once __DIR__ . '/exceptions/class-base-exception.php'; include_once __DIR__ . '/exceptions/class-api-exception.php'; include_once __DIR__ . '/exceptions/class-api-merchant-exception.php'; include_once __DIR__ . '/exceptions/class-connection-exception.php'; include_once __DIR__ . '/core/class-mode.php'; // Include core exceptions. include_once __DIR__ . '/core/exceptions/server/request/class-server-request-exception.php'; include_once __DIR__ . '/core/exceptions/server/request/class-invalid-request-parameter-exception.php'; include_once __DIR__ . '/core/exceptions/server/request/class-immutable-parameter-exception.php'; include_once __DIR__ . '/core/exceptions/server/request/class-extend-request-exception.php'; include_once __DIR__ . '/core/exceptions/server/response/class-server-response-exception.php'; // Include core requests. include_once __DIR__ . '/core/server/class-request.php'; include_once __DIR__ . '/core/server/class-response.php'; include_once __DIR__ . '/core/server/request/trait-intention.php'; include_once __DIR__ . '/core/server/request/trait-level3.php'; include_once __DIR__ . '/core/server/request/trait-order-info.php'; include_once __DIR__ . '/core/server/request/trait-date-parameters.php'; include_once __DIR__ . '/core/server/request/trait-use-test-mode-only-when-test-mode-onboarding.php'; include_once __DIR__ . '/core/server/request/class-generic.php'; include_once __DIR__ . '/core/server/request/class-get-intention.php'; include_once __DIR__ . '/core/server/request/class-create-intention.php'; include_once __DIR__ . '/core/server/request/class-update-intention.php'; include_once __DIR__ . '/core/server/request/class-capture-intention.php'; include_once __DIR__ . '/core/server/request/class-cancel-intention.php'; include_once __DIR__ . '/core/server/request/class-create-setup-intention.php'; include_once __DIR__ . '/core/server/request/class-create-and-confirm-setup-intention.php'; include_once __DIR__ . '/core/server/request/class-get-setup-intention.php'; include_once __DIR__ . '/core/server/request/class-get-account.php'; include_once __DIR__ . '/core/server/request/class-get-account-login-data.php'; include_once __DIR__ . '/core/server/request/class-get-account-capital-link.php'; include_once __DIR__ . '/core/server/request/class-add-account-tos-agreement.php'; include_once __DIR__ . '/core/server/request/class-update-account.php'; include_once __DIR__ . '/core/server/request/class-get-charge.php'; include_once __DIR__ . '/core/server/request/class-woopay-create-intent.php'; include_once __DIR__ . '/core/server/request/class-create-and-confirm-intention.php'; include_once __DIR__ . '/core/server/request/class-woopay-create-and-confirm-intention.php'; include_once __DIR__ . '/core/server/request/class-woopay-create-and-confirm-setup-intention.php'; include_once __DIR__ . '/core/server/request/class-paginated.php'; include_once __DIR__ . '/core/server/request/class-list-transactions.php'; include_once __DIR__ . '/core/server/request/class-list-fraud-outcome-transactions.php'; include_once __DIR__ . '/core/server/request/class-list-disputes.php'; include_once __DIR__ . '/core/server/request/class-list-deposits.php'; include_once __DIR__ . '/core/server/request/class-list-documents.php'; include_once __DIR__ . '/core/server/request/class-list-authorizations.php'; include_once __DIR__ . '/core/server/request/class-woopay-create-and-confirm-setup-intention.php'; include_once __DIR__ . '/core/server/request/class-refund-charge.php'; include_once __DIR__ . '/core/server/request/class-list-charge-refunds.php'; include_once __DIR__ . '/core/server/request/class-get-request.php'; include_once __DIR__ . '/core/server/request/class-request-utils.php'; include_once __DIR__ . '/core/server/request/class-get-pm-promotions.php'; include_once __DIR__ . '/core/server/request/class-activate-pm-promotion.php'; include_once __DIR__ . '/woopay/services/class-checkout-service.php'; self::$api_client = self::create_api_client(); include_once __DIR__ . '/compat/subscriptions/trait-wc-payments-subscriptions-utilities.php'; include_once __DIR__ . '/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php'; include_once __DIR__ . '/class-wc-payments-session-service.php'; include_once __DIR__ . '/class-wc-payments-redirect-service.php'; include_once __DIR__ . '/class-wc-payments-account.php'; include_once __DIR__ . '/class-wc-payments-customer-service.php'; include_once __DIR__ . '/class-logger.php'; include_once __DIR__ . '/class-logger-context.php'; include_once __DIR__ . '/class-session-rate-limiter.php'; include_once __DIR__ . '/class-wc-payment-gateway-wcpay.php'; include_once __DIR__ . '/class-wc-payments-checkout.php'; include_once __DIR__ . '/payment-methods/class-upe-payment-method.php'; include_once __DIR__ . '/payment-methods/class-cc-payment-method.php'; include_once __DIR__ . '/inline-script-payloads/class-woo-payments-payment-methods-config.php'; include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-button-helper.php'; include_once __DIR__ . '/class-wc-payment-token-wcpay-sepa.php'; include_once __DIR__ . '/class-wc-payments-status.php'; include_once __DIR__ . '/class-wc-payments-token-service.php'; include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-ajax-handler.php'; include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-button-display-handler.php'; include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-button-handler.php'; include_once __DIR__ . '/class-wc-payments-woopay-button-handler.php'; include_once __DIR__ . '/class-wc-payments-woopay-direct-checkout.php'; include_once __DIR__ . '/class-wc-payments-apple-pay-registration.php'; include_once __DIR__ . '/exceptions/class-add-payment-method-exception.php'; include_once __DIR__ . '/exceptions/class-amount-too-large-exception.php'; include_once __DIR__ . '/exceptions/class-amount-too-small-exception.php'; include_once __DIR__ . '/exceptions/class-cannot-combine-currencies-exception.php'; include_once __DIR__ . '/exceptions/class-intent-authentication-exception.php'; include_once __DIR__ . '/exceptions/class-invalid-payment-method-exception.php'; include_once __DIR__ . '/exceptions/class-process-payment-exception.php'; include_once __DIR__ . '/exceptions/class-invalid-phone-number-exception.php'; include_once __DIR__ . '/exceptions/class-invalid-webhook-data-exception.php'; include_once __DIR__ . '/exceptions/class-invalid-price-exception.php'; include_once __DIR__ . '/exceptions/class-fraud-ruleset-exception.php'; include_once __DIR__ . '/exceptions/class-fraud-prevention-enabled-exception.php'; include_once __DIR__ . '/exceptions/class-order-not-found-exception.php'; include_once __DIR__ . '/exceptions/class-order-id-mismatch-exception.php'; include_once __DIR__ . '/exceptions/class-rate-limiter-enabled-exception.php'; include_once __DIR__ . '/exceptions/class-invalid-address-exception.php'; include_once __DIR__ . '/exceptions/class-subscription-mode-mismatch-exception.php'; include_once __DIR__ . '/constants/class-base-constant.php'; include_once __DIR__ . '/constants/class-country-code.php'; include_once __DIR__ . '/constants/class-country-test-cards.php'; include_once __DIR__ . '/constants/class-currency-code.php'; include_once __DIR__ . '/constants/class-fraud-meta-box-type.php'; include_once __DIR__ . '/constants/class-order-mode.php'; include_once __DIR__ . '/constants/class-order-status.php'; include_once __DIR__ . '/constants/class-payment-type.php'; include_once __DIR__ . '/constants/class-payment-initiated-by.php'; include_once __DIR__ . '/constants/class-intent-status.php'; include_once __DIR__ . '/constants/class-refund-status.php'; include_once __DIR__ . '/constants/class-refund-failure-reason.php'; include_once __DIR__ . '/constants/class-payment-intent-status.php'; include_once __DIR__ . '/constants/class-payment-capture-type.php'; include_once __DIR__ . '/constants/class-payment-method.php'; include_once __DIR__ . '/constants/class-track-events.php'; include_once __DIR__ . '/class-payment-information.php'; require_once __DIR__ . '/notes/class-wc-payments-remote-note-service.php'; include_once __DIR__ . '/class-wc-payments-action-scheduler-service.php'; include_once __DIR__ . '/class-wc-payments-fraud-service.php'; include_once __DIR__ . '/class-wc-payments-onboarding-service.php'; include_once __DIR__ . '/class-experimental-abtest.php'; include_once __DIR__ . '/class-wc-payments-localization-service.php'; include_once __DIR__ . '/class-wc-payments-settings-service.php'; include_once __DIR__ . '/in-person-payments/class-wc-payments-in-person-payments-receipts-service.php'; include_once __DIR__ . '/class-wc-payments-order-service.php'; include_once __DIR__ . '/class-wc-payments-order-success-page.php'; include_once __DIR__ . '/class-wc-payments-file-service.php'; include_once __DIR__ . '/migrations/class-wc-payments-remediate-canceled-auth-fees.php'; include_once __DIR__ . '/class-wc-payments-webhook-processing-service.php'; include_once __DIR__ . '/class-wc-payments-webhook-reliability-service.php'; include_once __DIR__ . '/fraud-prevention/class-fraud-prevention-service.php'; include_once __DIR__ . '/fraud-prevention/class-buyer-fingerprinting-service.php'; include_once __DIR__ . '/fraud-prevention/class-fraud-risk-tools.php'; include_once __DIR__ . '/fraud-prevention/wc-payments-fraud-risk-tools.php'; include_once __DIR__ . '/woopay/class-woopay-store-api-token.php'; include_once __DIR__ . '/woopay/class-woopay-utilities.php'; include_once __DIR__ . '/woopay/class-woopay-order-status-sync.php'; include_once __DIR__ . '/woopay/class-woopay-store-api-session-handler.php'; include_once __DIR__ . '/woopay/class-woopay-scheduler.php'; include_once __DIR__ . '/woopay/class-woopay-adapted-extensions.php'; include_once __DIR__ . '/class-wc-payment-token-wcpay-link.php'; include_once __DIR__ . '/core/service/class-wc-payments-customer-service-api.php'; include_once __DIR__ . '/class-duplicate-payment-prevention-service.php'; include_once __DIR__ . '/class-wc-payments-incentives-service.php'; include_once __DIR__ . '/class-wc-payments-pm-promotions-service.php'; include_once __DIR__ . '/class-compatibility-service.php'; include_once __DIR__ . '/compat/multi-currency/wc-payments-multi-currency.php'; include_once __DIR__ . '/compat/multi-currency/class-wc-payments-currency-manager.php'; include_once __DIR__ . '/class-duplicates-detection-service.php'; include_once __DIR__ . '/class-wc-payments-payment-method-service.php'; wcpay_get_container()->get( \WCPay\Internal\LoggerContext::class )->init_hooks(); self::$woopay_checkout_service = new Checkout_Service(); self::$woopay_checkout_service->init(); // // Load woopay save user section if feature is enabled. add_action( 'woocommerce_cart_loaded_from_session', [ __CLASS__, 'init_woopay' ] ); // Init the email template for In Person payment receipt email. We need to do it before passing the mailer to the service. add_filter( 'woocommerce_email_classes', [ __CLASS__, 'add_ipp_emails' ], 10 ); // Always load tracker to avoid class not found errors. include_once WCPAY_ABSPATH . 'includes/admin/tracks/class-tracker.php'; // Load woopay tracking. include_once WCPAY_ABSPATH . 'includes/class-woopay-tracker.php'; self::$order_service = new WC_Payments_Order_Service( self::$api_client ); self::$compatibility_service = new Compatibility_Service( self::$api_client ); self::$action_scheduler_service = new WC_Payments_Action_Scheduler_Service( self::$api_client, self::$order_service, self::$compatibility_service ); self::$session_service = new WC_Payments_Session_Service( self::$api_client ); self::$redirect_service = new WC_Payments_Redirect_Service( self::$api_client ); self::$onboarding_service = new WC_Payments_Onboarding_Service( self::$api_client, self::$database_cache, self::$session_service ); self::$account = new WC_Payments_Account( self::$api_client, self::$database_cache, self::$action_scheduler_service, self::$onboarding_service, self::$redirect_service ); self::$customer_service = new WC_Payments_Customer_Service( self::$api_client, self::$account, self::$session_service, self::$order_service ); self::$token_service = new WC_Payments_Token_Service( self::$api_client, self::$customer_service ); self::$remote_note_service = new WC_Payments_Remote_Note_Service( WC_Data_Store::load( 'admin-note' ) ); self::$fraud_service = new WC_Payments_Fraud_Service( self::$api_client, self::$customer_service, self::$account, self::$session_service, self::$database_cache ); self::$in_person_payments_receipts_service = new WC_Payments_In_Person_Payments_Receipts_Service(); self::$localization_service = new WC_Payments_Localization_Service(); self::$settings_service = new WC_Payments_Settings_Service(); self::$failed_transaction_rate_limiter = new Session_Rate_Limiter( Session_Rate_Limiter::SESSION_KEY_DECLINED_CARD_REGISTRY, 5, 10 * MINUTE_IN_SECONDS ); self::$order_success_page = new WC_Payments_Order_Success_Page(); self::$woopay_util = new WooPay_Utilities(); self::$woopay_tracker = new WooPay_Tracker( self::get_wc_payments_http() ); self::$incentives_service = new WC_Payments_Incentives_Service( self::$database_cache ); self::$pm_promotions_service = new WC_Payments_PM_Promotions_Service( null, self::$account ); self::$duplicate_payment_prevention_service = new Duplicate_Payment_Prevention_Service(); self::$duplicates_detection_service = new Duplicates_Detection_Service(); self::$fee_remediation = new WC_Payments_Remediate_Canceled_Auth_Fees(); ( new WooPay_Scheduler( self::$api_client ) )->init(); // Initialise hooks. self::$action_scheduler_service->init_hooks(); self::$account->init_hooks(); self::$fraud_service->init_hooks(); self::$onboarding_service->init_hooks(); self::$incentives_service->init_hooks(); self::$pm_promotions_service->init_hooks(); self::$compatibility_service->init_hooks(); self::$customer_service->init_hooks(); self::$token_service->init_hooks(); self::$fee_remediation->init(); /** * FLAG: PAYMENT_METHODS_LIST * As payment methods are converted to use definitions, they need to be removed from the list below. */ $payment_method_classes = [ CC_Payment_Method::class, ]; $payment_methods = []; // Initialize legacy payment methods. foreach ( $payment_method_classes as $payment_method_class ) { $payment_method = new $payment_method_class( self::$token_service ); $payment_methods[ $payment_method->get_id() ] = $payment_method; } // Initialize definition-based payment methods. // Initialize and get payment method classes from the registry for those that have been converted. $registry = PaymentMethodDefinitionRegistry::instance(); $registry->init(); $payment_method_definitions = $registry->get_all_payment_method_definitions(); foreach ( $payment_method_definitions as $definition_class ) { $payment_method = new UPE_Payment_Method( self::$token_service, $definition_class ); $payment_methods[ $payment_method->get_id() ] = $payment_method; } foreach ( $payment_methods as $payment_method ) { self::$payment_method_map[ $payment_method->get_id() ] = $payment_method; $split_gateway = new WC_Payment_Gateway_WCPay( self::$api_client, self::$account, self::$customer_service, self::$token_service, self::$action_scheduler_service, $payment_method, $payment_methods, self::$order_service, self::$duplicate_payment_prevention_service, self::$localization_service, self::$fraud_service, self::$duplicates_detection_service, self::$failed_transaction_rate_limiter ); // Card gateway hooks are registered once below. if ( 'card' !== $payment_method->get_id() ) { $split_gateway->init_hooks(); } self::$payment_gateway_map[ $payment_method->get_id() ] = $split_gateway; } self::$card_gateway = self::get_payment_gateway_by_id( 'card' ); self::$wc_payments_checkout = new WC_Payments_Checkout( self::get_gateway(), self::$woopay_util, self::$account, self::$customer_service, self::$fraud_service ); self::$card_gateway->init_hooks(); self::$wc_payments_checkout->init_hooks(); self::$webhook_processing_service = new WC_Payments_Webhook_Processing_Service( self::$api_client, self::$db_helper, self::$account, self::$remote_note_service, self::$order_service, self::$in_person_payments_receipts_service, self::get_gateway(), self::$database_cache, self::$onboarding_service, self::$token_service ); self::$webhook_reliability_service = new WC_Payments_Webhook_Reliability_Service( self::$api_client, self::$action_scheduler_service, self::$webhook_processing_service ); self::$customer_service_api = new WC_Payments_Customer_Service_API( self::$customer_service, self::$token_service ); self::$currency_manager = new WC_Payments_Currency_Manager( self::get_gateway() ); self::$currency_manager->init_hooks(); // Only register hooks of the new `src` service with the same feature of Duplicate_Payment_Prevention_Service. // To avoid register the same hooks twice. wcpay_get_container()->get( \WCPay\Internal\Service\DuplicatePaymentPreventionService::class )->init_hooks(); self::$apple_pay_registration = new WC_Payments_Apple_Pay_Registration( self::$api_client, self::$account, self::get_gateway() ); self::$apple_pay_registration->init_hooks(); $express_checkout_helper = new WC_Payments_Express_Checkout_Button_Helper( self::get_gateway(), self::$account ); self::set_express_checkout_helper( $express_checkout_helper ); self::$payment_method_service = new WC_Payments_Payment_Method_Service( self::$api_client, self::$order_service ); self::$payment_method_service->init_hooks(); // Delay registering hooks that could end up in a fatal error due to expired account cache. // The `woocommerce_payments_account_refreshed` action will result in a fatal error if it's fired before the `$wp_rewrite` is defined. // See #8942 for more details. add_action( 'setup_theme', function () { add_action( 'woocommerce_payments_account_refreshed', [ WooPay_Order_Status_Sync::class, 'remove_webhook' ] ); self::maybe_register_woopay_hooks(); self::maybe_init_woopay_direct_checkout(); self::maybe_enqueue_woopay_common_config_script( WC_Payments_Features::is_woopay_direct_checkout_enabled() ); } ); // Initialize express checkout buttons at 'init' priority 15 (after Multi-Currency determines the // customer's selected currency at priorities 11-12). This ensures can_use_amazon_pay() checks // availability against the correct presentment currency, not the store's default currency. add_action( 'init', [ __CLASS__, 'maybe_display_express_checkout_buttons' ], 15 ); if ( self::get_gateway()->is_enabled() ) { // Insert the Stripe Payment Messaging Element only if there is at least one BNPL method enabled. $enabled_bnpl_payment_methods = array_intersect( Payment_Method::BNPL_PAYMENT_METHODS, self::get_gateway()->get_upe_enabled_payment_method_ids() ); if ( [] !== $enabled_bnpl_payment_methods ) { add_action( 'woocommerce_single_product_summary', [ __CLASS__, 'load_stripe_bnpl_site_messaging' ], 10 ); add_action( 'woocommerce_proceed_to_checkout', [ __CLASS__, 'load_stripe_bnpl_site_messaging' ], 5 ); add_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after', [ __CLASS__, 'load_stripe_bnpl_site_messaging' ] ); add_action( 'wc_ajax_wcpay_get_cart_total', [ __CLASS__, 'ajax_get_cart_total' ] ); add_action( 'wc_ajax_wcpay_check_bnpl_availability', [ __CLASS__, 'ajax_check_bnpl_availability' ] ); } } add_filter( 'woocommerce_payment_gateways', [ __CLASS__, 'register_gateway' ] ); add_filter( 'option_woocommerce_gateway_order', [ __CLASS__, 'order_woopayments_gateways' ], 2 ); add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'order_woopayments_gateways' ], 3 ); add_filter( 'woocommerce_admin_get_user_data_fields', [ __CLASS__, 'add_user_data_fields' ] ); add_filter( 'woocommerce_address_providers', [ __CLASS__, 'add_address_provider' ] ); // Add note query support for source. add_filter( 'woocommerce_rest_notes_object_query', [ __CLASS__, 'possibly_add_source_to_notes_query' ], 10, 2 ); add_filter( 'woocommerce_note_where_clauses', [ __CLASS__, 'possibly_add_note_source_where_clause' ], 10, 2 ); // Priority 5 so we can manipulate the registered gateways before they are shown. add_action( 'woocommerce_admin_field_payment_gateways', [ __CLASS__, 'hide_gateways_on_settings_page' ], 5 ); require_once __DIR__ . '/migrations/class-allowed-payment-request-button-types-update.php'; require_once __DIR__ . '/migrations/class-allowed-payment-request-button-sizes-update.php'; require_once __DIR__ . '/migrations/class-update-service-data-from-server.php'; require_once __DIR__ . '/migrations/class-additional-payment-methods-admin-notes-removal.php'; require_once __DIR__ . '/migrations/class-link-woopay-mutual-exclusion-handler.php'; require_once __DIR__ . '/migrations/class-gateway-settings-sync.php'; require_once __DIR__ . '/migrations/class-delete-active-woopay-webhook.php'; require_once __DIR__ . '/migrations/class-payment-method-deprecation-settings-update.php'; require_once __DIR__ . '/migrations/class-erase-bnpl-announcement-meta.php'; require_once __DIR__ . '/migrations/class-erase-deprecated-flags-and-options.php'; require_once __DIR__ . '/migrations/class-manual-capture-payment-method-settings-update.php'; require_once __DIR__ . '/migrations/class-migrate-payment-request-to-express-checkout-enabled.php'; require_once __DIR__ . '/migrations/class-migrate-express-checkout-locations.php'; require_once __DIR__ . '/migrations/class-add-amazon-pay-to-express-checkout-locations.php'; add_action( 'woocommerce_woocommerce_payments_updated', [ new Allowed_Payment_Request_Button_Types_Update( self::get_gateway() ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Allowed_Payment_Request_Button_Sizes_Update( self::get_gateway() ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Update_Service_Data_From_Server( self::get_account_service() ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Additional_Payment_Methods_Admin_Notes_Removal(), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Link_WooPay_Mutual_Exclusion_Handler( self::get_gateway() ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Gateway_Settings_Sync( self::get_gateway(), self::get_payment_gateway_map() ), 'maybe_sync' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ '\WCPay\Migrations\Delete_Active_WooPay_Webhook', 'maybe_delete' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Payment_Method_Deprecation_Settings_Update( self::get_gateway(), self::get_payment_gateway_map(), GiropayDefinition::get_id(), '7.9.0' ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Payment_Method_Deprecation_Settings_Update( self::get_gateway(), self::get_payment_gateway_map(), SofortDefinition::get_id(), '8.9.0' ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Erase_Bnpl_Announcement_Meta(), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Erase_Deprecated_Flags_And_Options(), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Manual_Capture_Payment_Method_Settings_Update( self::get_gateway(), self::get_payment_gateway_map() ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Migrate_Payment_Request_To_Express_Checkout_Enabled(), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Migrate_Express_Checkout_Locations(), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Add_Amazon_Pay_To_Express_Checkout_Locations(), 'maybe_migrate' ] ); include_once WCPAY_ABSPATH . '/includes/class-wc-payments-explicit-price-formatter.php'; WC_Payments_Explicit_Price_Formatter::init(); include_once WCPAY_ABSPATH . 'includes/class-wc-payments-captured-event-note.php'; include_once WCPAY_ABSPATH . 'includes/admin/class-wc-payments-admin-settings.php'; include_once WCPAY_ABSPATH . 'includes/fraud-prevention/class-order-fraud-and-risk-meta-box.php'; // Add admin screens. if ( is_admin() ) { include_once WCPAY_ABSPATH . 'includes/inline-script-payloads/class-woo-payments-payment-method-definitions.php'; include_once WCPAY_ABSPATH . 'includes/admin/class-wc-payments-admin.php'; } if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) { $admin = new WC_Payments_Admin( self::$api_client, self::get_gateway(), self::$account, self::$onboarding_service, self::$order_service, self::$incentives_service, self::$pm_promotions_service, self::$fraud_service, self::$database_cache ); $admin->init_hooks(); $admin_settings = new WC_Payments_Admin_Settings( self::get_gateway(), self::get_account_service() ); $admin_settings->init_hooks(); // Use tracks loader only in admin screens because it relies on WC_Tracks loaded by WC_Admin. include_once WCPAY_ABSPATH . 'includes/admin/tracks/tracks-loader.php'; $wcpay_status = new WC_Payments_Status( self::get_gateway(), self::get_wc_payments_http(), self::get_account_service() ); $wcpay_status->init_hooks(); $wcpay_order_frt_meta_box = new WCPay\Fraud_Prevention\Order_Fraud_And_Risk_Meta_Box( self::$order_service ); $wcpay_order_frt_meta_box->init_hooks(); // Initialize Stripe Billing deprecation notices. include_once WCPAY_ABSPATH . '/includes/subscriptions/class-wc-payments-subscriptions-admin-notices.php'; $wcpay_subscriptions_admin_notices = new WC_Payments_Subscriptions_Admin_Notices(); $wcpay_subscriptions_admin_notices->init_hooks(); } // Load Stripe Billing subscription integration. include_once WCPAY_ABSPATH . '/includes/subscriptions/class-wc-payments-subscriptions.php'; WC_Payments_Subscriptions::init( self::$api_client, self::$customer_service, self::$order_service, self::$account, self::$token_service ); // Add hook to remove Stripe Billing deprecation note when WooCommerce Subscriptions is installed. add_action( 'activated_plugin', [ __CLASS__, 'maybe_remove_stripe_billing_deprecation_note' ], 10, 1 ); if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '7.9.0', '<' ) ) { add_action( 'woocommerce_onboarding_profile_data_updated', 'WC_Payments_Features::maybe_enable_wcpay_subscriptions_after_onboarding', 10, 2 ); } add_action( 'woocommerce_woocommerce_payments_updated', [ __CLASS__, 'maybe_disable_wcpay_subscriptions_on_update' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ __CLASS__, 'maybe_update_stripe_billing_deprecation_note' ] ); add_action( 'rest_api_init', [ __CLASS__, 'init_rest_api' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ __CLASS__, 'set_plugin_activation_timestamp' ] ); add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_dev_runtime_scripts' ] ); add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_assets_script' ] ); add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_assets_script' ] ); add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_cart_scripts' ] ); self::$duplicate_payment_prevention_service->init( self::$card_gateway, self::$order_service ); wcpay_get_container()->get( \WCPay\Internal\PluginManagement\TranslationsLoader::class )->init_hooks(); } /** * Returns the gateway's working mode. * * @return Mode */ public static function mode() { return self::$mode; } /** * Adds IPP Email template to WooCommerce emails. * * @param array $email_classes the email classes. * @return array */ public static function add_ipp_emails( array $email_classes ): array { $email_classes['WC_Payments_Email_IPP_Receipt'] = include __DIR__ . '/emails/class-wc-payments-email-ipp-receipt.php'; return $email_classes; } /** * Prints the given message in an "admin notice" wrapper with "error" class. * * @param string $message Message to print. Can contain HTML. */ public static function display_admin_error( $message ) { self::display_admin_notice( $message, 'notice-error' ); } /** * Prints the given message in an "admin notice" wrapper with provided classes. * * @param string $message Message to print. Can contain HTML. * @param string $classes Space separated list of classes to be applied to notice element. */ public static function display_admin_notice( $message, $classes ) { ?> <div class="notice wcpay-notice <?php echo esc_attr( $classes ); ?>"> <p><b>WooPayments</b></p> <p><?php echo $message; // PHPCS:Ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p> </div> <?php } /** * Get plugin headers and cache the result to avoid reopening the file. * First call should execute get_file_data and fetch headers from plugin details comment. * Subsequent calls return the value stored in the variable $plugin_headers. * * @return array Array with plugin headers */ public static function get_plugin_headers() { if ( null === self::$plugin_headers ) { self::$plugin_headers = get_file_data( WCPAY_PLUGIN_FILE, [ // Mirrors the functionality on WooCommerce core: https://github.com/woocommerce/woocommerce/blob/ff2eadeccec64aa76abd02c931bf607dd819bbf0/includes/wc-core-functions.php#L1916 . 'WCRequires' => 'WC requires at least', 'RequiresWP' => 'Requires at least', 'Version' => 'Version', ] ); } return self::$plugin_headers; } /** * Adds the WooPayments' gateway class to the list of installed payment gateways. * * @param array $gateways Existing list of gateway classes that will be available for the merchant to configure. * @return array The list of payment gateways that will be available, including WooPayments' Gateway class. */ public static function register_gateway( $gateways ) { $payment_methods = array_keys( self::get_payment_method_map() ); $gateways[] = self::$card_gateway; foreach ( $payment_methods as $payment_method_id ) { if ( 'card' === $payment_method_id || 'link' === $payment_method_id ) { continue; } $gateways[] = self::get_payment_gateway_by_id( $payment_method_id ); } return $gateways; } /** * Add the WooCommerce Payments address autocomplete provider, but only if a WCPay gateway is enabled. * * @param array $providers The address providers. * @return array The address providers. */ public static function add_address_provider( $providers ) { // Only enable address provider integration if a WCPay gateway is enabled. if ( ! self::get_gateway()->is_enabled() ) { return $providers; } if ( ! class_exists( 'Automattic\WooCommerce\Internal\AddressProvider\AbstractAutomatticAddressProvider' ) ) { return $providers; } include_once __DIR__ . '/class-wc-payments-address-provider.php'; $providers[] = new WC_Payments_Address_Provider( self::$api_client, self::$account, self::$database_cache ); return $providers; } /** * Called on Payments setting page. * * Remove all WCPay gateways except CC one. */ public static function hide_gateways_on_settings_page() { $default_gateway = self::get_gateway(); foreach ( WC()->payment_gateways->payment_gateways as $index => $payment_gateway ) { if ( $payment_gateway instanceof WC_Payment_Gateway_WCPay && $payment_gateway !== $default_gateway ) { unset( WC()->payment_gateways->payment_gateways[ $index ] ); } } } /** * Sets WooPayments gateways at the beginning if not already in the ordering. * Sets WooPayments gateways after the main gateway if already in the ordering. * * @param array $ordering Existing ordering of the payment gateways. * * @return array Modified ordering. */ public static function order_woopayments_gateways( $ordering ) { try { $ordering = (array) $ordering; $woopayments_payment_methods = array_flip( self::get_woopayments_gateway_ids() ); $main_gateway_id = self::get_gateway()->id; $main_gateway_position = $ordering[ $main_gateway_id ] ?? null; $before = []; $after = []; foreach ( $ordering as $gateway_id => $position ) { if ( null === $main_gateway_position || $position < $main_gateway_position ) { $before[ $gateway_id ] = null; // `null` for now, the position will be set later. } elseif ( $position > $main_gateway_position && ! isset( $woopayments_payment_methods[ $gateway_id ] ) ) { $after[ $gateway_id ] = null; // `null` for now, the position will be set later. } } $new_ordering = []; if ( null === $main_gateway_position ) { $new_ordering = array_merge( $woopayments_payment_methods, $before, $after ); } else { $new_ordering = array_merge( $before, $woopayments_payment_methods, $after ); } $index = 0; foreach ( array_keys( $new_ordering ) as $gateway_id ) { $new_ordering[ $gateway_id ] = $index++; } return $new_ordering; } catch ( Exception $e ) { WC_Payments_Utils::log_to_wc( 'Failed to order gateways: ' . $e->getMessage(), 'warning' ); return $ordering; } } /** * Define fields for storing user preferences in the wp_usermeta table. * * Includes fields for inbox notifications and table column visibility. * * @param array $user_data_fields User data fields. * @return array */ public static function add_user_data_fields( $user_data_fields ) { return array_merge( $user_data_fields, [ // Inbox notifications. 'wc_payments_overview_inbox_last_read', // Column visibility preferences. 'wc_payments_transactions_hidden_columns', 'wc_payments_transactions_blocked_hidden_columns', 'wc_payments_transactions_risk_review_hidden_columns', 'wc_payments_transactions_uncaptured_hidden_columns', 'wc_payments_payouts_hidden_columns', 'wc_payments_disputes_hidden_columns', 'wc_payments_documents_hidden_columns', // WPORG 2025 merchant feedback prompt user dismissed state. 'wc_payments_wporg_review_2025_prompt_dismissed', ] ); } /** * By default, new payment gateways are put at the bottom of the list on the admin "Payments" settings screen. * For visibility, we want WooPayments to be at the top of the list. * NOTE: this can be removed after WC version 5.6, when the api supports the use of source. * https://github.com/woocommerce/woocommerce-admin/pull/6979 * * @param array $args Existing ordering of the payment gateways. * @param WP_REST_Request $request Full details about the request. * * @return array Modified ordering. */ public static function possibly_add_source_to_notes_query( $args, $request ) { if ( isset( $request['source'] ) && ! isset( $args['source'] ) ) { return array_merge( $args, [ 'source' => wp_parse_list( $request['source'] ), ] ); } return $args; } /** * Adds source where clause to note query. * NOTE: this can be removed after WC version 5.6, when the api supports the use of source. * https://github.com/woocommerce/woocommerce-admin/pull/6979 * * @param string $where_clauses Existing ordering of the payment gateways. * @param array $args Full details about the request. * * @return string Modified where clause. */ public static function possibly_add_note_source_where_clause( $where_clauses, $args ) { if ( ! empty( $args['source'] ) && false === strpos( $where_clauses, 'AND source IN' ) ) { $where_source_array = []; foreach ( $args['source'] as $args_type ) { $args_type = trim( $args_type ); $where_source_array[] = "'" . esc_sql( $args_type ) . "'"; } $escaped_where_source = implode( ',', $where_source_array ); $where_clauses .= " AND source IN ($escaped_where_source)"; } return $where_clauses; } /** * Create the API client. * * @return WC_Payments_API_Client */ public static function create_api_client() { require_once __DIR__ . '/wc-payment-api/models/class-wc-payments-api-charge.php'; require_once __DIR__ . '/wc-payment-api/models/class-wc-payments-api-abstract-intention.php'; require_once __DIR__ . '/wc-payment-api/models/class-wc-payments-api-payment-intention.php'; require_once __DIR__ . '/wc-payment-api/models/class-wc-payments-api-setup-intention.php'; require_once __DIR__ . '/wc-payment-api/class-wc-payments-api-client.php'; $http_class = self::get_wc_payments_http(); $api_client_class = apply_filters( 'wc_payments_api_client', WC_Payments_API_Client::class ); if ( ! class_exists( $api_client_class ) || ! is_subclass_of( $api_client_class, 'WC_Payments_API_Client' ) ) { $api_client_class = WC_Payments_API_Client::class; } return new $api_client_class( 'WooCommerce Payments/' . WCPAY_VERSION_NUMBER, $http_class, self::$db_helper ); } /** * Initialize the REST API controllers. */ public static function init_rest_api() { // Ensures we are not initializing our REST during `rest_preload_api_request`. // When constructor signatures change, in manual update scenarios we were running into fatals. // Those fatals are not critical, but they cause hiccups in the release process as catches unnecessary attention. if ( function_exists( 'get_current_screen' ) && get_current_screen() ) { return; } include_once WCPAY_ABSPATH . 'includes/exceptions/class-rest-request-exception.php'; include_once WCPAY_ABSPATH . 'includes/admin/class-wc-payments-rest-controller.php'; include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-accounts-controller.php'; $accounts_controller = new WC_REST_Payments_Accounts_Controller( self::$api_client ); $accounts_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-deposits-controller.php'; $deposits_controller = new WC_REST_Payments_Deposits_Controller( self::$api_client ); $deposits_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-transactions-controller.php'; $transactions_controller = new WC_REST_Payments_Transactions_Controller( self::$api_client ); $transactions_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-disputes-controller.php'; $disputes_controller = new WC_REST_Payments_Disputes_Controller( self::$api_client ); $disputes_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-charges-controller.php'; $charges_controller = new WC_REST_Payments_Charges_Controller( self::$api_client ); $charges_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-connection-tokens-controller.php'; $conn_tokens_controller = new WC_REST_Payments_Connection_Tokens_Controller( self::$api_client ); $conn_tokens_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-orders-controller.php'; $orders_controller = new WC_REST_Payments_Orders_Controller( self::$api_client, self::get_gateway(), self::$customer_service, self::$order_service ); $orders_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-fraud-outcomes-controller.php'; $fraud_outcomes_controller = new WC_REST_Payments_Fraud_Outcomes_Controller( self::$api_client ); $fraud_outcomes_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-timeline-controller.php'; $timeline_controller = new WC_REST_Payments_Timeline_Controller( self::$api_client ); $timeline_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-webhook-controller.php'; $webhook_controller = new WC_REST_Payments_Webhook_Controller( self::$api_client, self::$webhook_processing_service ); $webhook_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-tos-controller.php'; $tos_controller = new WC_REST_Payments_Tos_Controller( self::$api_client, self::get_gateway(), self::$account ); $tos_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-terminal-locations-controller.php'; $accounts_controller = new WC_REST_Payments_Terminal_Locations_Controller( self::$api_client ); $accounts_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-settings-controller.php'; $settings_controller = new WC_REST_Payments_Settings_Controller( self::$api_client, self::get_gateway(), self::$account, self::$pm_promotions_service ); $settings_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-settings-option-controller.php'; $settings_option_controller = new WC_REST_Payments_Settings_Option_Controller( self::$api_client ); $settings_option_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-reader-controller.php'; $charges_controller = new WC_REST_Payments_Reader_Controller( self::$api_client, self::get_gateway(), self::$in_person_payments_receipts_service ); $charges_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-files-controller.php'; $files_controller = new WC_REST_Payments_Files_Controller( self::$api_client ); $files_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-capital-controller.php'; $capital_controller = new WC_REST_Payments_Capital_Controller( self::$api_client ); $capital_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-pm-promotions-controller.php'; $promotions_controller = new WC_REST_Payments_PM_Promotions_Controller( self::$api_client, self::$pm_promotions_service ); $promotions_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-onboarding-controller.php'; $onboarding_controller = new WC_REST_Payments_Onboarding_Controller( self::$api_client, self::$onboarding_service ); $onboarding_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-customer-controller.php'; $customer_controller = new WC_REST_Payments_Customer_Controller( self::$api_client, self::$customer_service ); $customer_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-refunds-controller.php'; $refunds_controller = new WC_REST_Payments_Refunds_Controller( self::$api_client ); $refunds_controller->register_routes(); if ( WC_Payments_Features::is_documents_section_enabled() ) { include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-documents-controller.php'; $documents_controller = new WC_REST_Payments_Documents_Controller( self::$api_client ); $documents_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-vat-controller.php'; $vat_controller = new WC_REST_Payments_VAT_Controller( self::$api_client ); $vat_controller->register_routes(); } include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-payment-intents-controller.php'; $payment_intents_controller = new WC_REST_Payments_Payment_Intents_Controller( self::$api_client, self::get_gateway(), wcpay_get_container()->get( OrderService::class ), wcpay_get_container()->get( Level3Service::class ) ); $payment_intents_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-authorizations-controller.php'; $authorizations_controller = new WC_REST_Payments_Authorizations_Controller( self::$api_client ); $authorizations_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/reports/class-wc-rest-payments-reports-transactions-controller.php'; $reports_transactions_controller = new WC_REST_Payments_Reports_Transactions_Controller( self::$api_client ); $reports_transactions_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/reports/class-wc-rest-payments-reports-authorizations-controller.php'; $reports_authorizations_controller = new WC_REST_Payments_Reports_Authorizations_Controller( self::$api_client ); $reports_authorizations_controller->register_routes(); include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-woopay-session-controller.php'; $woopay_session_controller = new WC_REST_WooPay_Session_Controller(); $woopay_session_controller->register_routes(); } /** * Gets the file modified time as a cache buster if we're in dev mode, or the plugin version otherwise. * * @param string $file Local path to the file. * @return string The cache buster value to use for the given file. */ public static function get_file_version( $file ): string { if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( WCPAY_ABSPATH . $file ) ) { return (string) filemtime( WCPAY_ABSPATH . trim( $file, '/' ) ); } return WCPAY_VERSION_NUMBER; } /** * Returns the WooPay_Tracker instance * * @return WooPay_Tracker instance */ public static function woopay_tracker(): WooPay_Tracker { return self::$woopay_tracker; } /** * Load script with all required dependencies. * * @param string $handler Script handler. * @param string $script Script name. * @param array $dependencies Additional dependencies. * * @return void */ public static function register_script_with_dependencies( string $handler, string $script, array $dependencies = [] ) { $script_file = $script . '.js'; $script_src_url = plugins_url( $script_file, WCPAY_PLUGIN_FILE ); $script_asset_path = WCPAY_ABSPATH . $script . '.asset.php'; $script_asset = file_exists( $script_asset_path ) ? require $script_asset_path : [ 'dependencies' => [] ]; // nosemgrep: audit.php.lang.security.file.inclusion-arg -- server generated path is used. $script_asset['dependencies'] = array_merge( $script_asset['dependencies'], $dependencies ); wp_register_script( $handler, $script_src_url, $script_asset['dependencies'], self::get_file_version( $script_file ), true ); } /** * Returns payment method instance by Stripe ID. * * @param string $payment_method_id Stripe payment method type ID. * @return false|UPE_Payment_Method Matching Payment Method instance. */ public static function get_payment_method_by_id( $payment_method_id ) { if ( ! isset( self::$payment_method_map[ $payment_method_id ] ) ) { return false; } return self::$payment_method_map[ $payment_method_id ]; } /** * Returns payment gateway instance by Stripe ID. * * @param string $payment_method_id Stripe payment method type ID. * @return false|WC_Payment_Gateway_WCPay Matching Payment Gateway instance. */ public static function get_payment_gateway_by_id( $payment_method_id ) { if ( ! isset( self::$payment_gateway_map[ $payment_method_id ] ) ) { return false; } return self::$payment_gateway_map[ $payment_method_id ]; } /** * Returns the WooPayments gateway IDs. * * @return array */ public static function get_woopayments_gateway_ids() { $wcpay_gateway_ids = []; foreach ( self::get_payment_gateway_map() as $gateway ) { $wcpay_gateway_ids[] = $gateway->id; } return $wcpay_gateway_ids; } /** * Returns Payment Method map. * * @return array */ public static function get_payment_method_map() { return self::$payment_method_map; } /** * Returns Payment Gateway map. * * @return array */ public static function get_payment_gateway_map() { return self::$payment_gateway_map; } /** * Returns the WC_Payment_Gateway_WCPay instance * * @return WC_Payment_Gateway_WCPay gateway instance */ public static function get_gateway() { return self::$card_gateway; } /** * Returns the WC_Payments_Checkout instance * * @return WC_Payments_Checkout gateway instance */ public static function get_wc_payments_checkout() { return self::$wc_payments_checkout; } /** * Returns the WC_Payments_Express_Checkout_Button_Helper instance. * * @return WC_Payments_Express_Checkout_Button_Helper instance. */ public static function get_express_checkout_helper() { return self::$express_checkout_helper; } /** * Returns the Database_Cache instance. * * @return Database_Cache Database_Cache instance. */ public static function get_database_cache(): Database_Cache { return self::$database_cache; } /** * Sets the Database_Cache instance. * * @param Database_Cache $database_cache The cache instance. */ public static function set_database_cache( Database_Cache $database_cache ) { self::$database_cache = $database_cache; } /** * Sets the card gateway instance. * * @param WC_Payment_Gateway_WCPay $gateway The card gateway instance. */ public static function set_gateway( $gateway ) { self::$card_gateway = $gateway; } /** * Sets the express checkout helper instance. * * @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper The express checkout helper instance. */ public static function set_express_checkout_helper( $express_checkout_helper ) { self::$express_checkout_helper = $express_checkout_helper; } /** * Returns the WC_Payments_Account instance * * @return WC_Payments_Account account service instance */ public static function get_account_service() { return self::$account; } /** * Sets the account service instance. * * @param WC_Payments_Account $account The account instance. */ public static function set_account_service( WC_Payments_Account $account ) { self::$account = $account; } /** * Returns the WC_Payments_API_Client * * @return WC_Payments_API_Client API Client instance */ public static function get_payments_api_client() { return self::$api_client; } /** * Returns the WC_Payments_Localization_Service * * @return WC_Payments_Localization_Service Localization Service instance */ public static function get_localization_service() { return self::$localization_service; } /** * Returns the WC_Payments_Settings_Service * * @return WC_Payments_Settings_Service Localization Service instance */ public static function get_settings_service() { return self::$settings_service; } /** * Returns the WC_Payments_Action_Scheduler_Service * * @return WC_Payments_Action_Scheduler_Service Action Scheduler Service instance */ public static function get_action_scheduler_service() { return self::$action_scheduler_service; } /** * Returns the WC_Payments_Fraud_Service instance * * @return WC_Payments_Fraud_Service Fraud Service instance */ public static function get_fraud_service() { return self::$fraud_service; } /** * Returns the WC_Payments_Customer_Service instance * * @return WC_Payments_Customer_Service The Customer Service instance. */ public static function get_customer_service(): WC_Payments_Customer_Service { return self::$customer_service; } /** * Returns the WC_Payments_Customer_Service_API instance * * @return WC_Payments_Customer_Service_API The Customer Service instance. */ public static function get_customer_service_api(): WC_Payments_Customer_Service_API { return self::$customer_service_api; } /** * Returns the order service instance. * * @return WC_Payments_Order_Service */ public static function get_order_service(): WC_Payments_Order_Service { return self::$order_service; } /** * Returns the token service instance. * * @return WC_Payments_Token_Service */ public static function get_token_service(): WC_Payments_Token_Service { return self::$token_service; } /** * Sets the token service instance. This is needed only for tests. * * @param WC_Payments_Token_Service $token_service Instance of WC_Payments_Token_Service. * * @return void */ public static function set_token_service( WC_Payments_Token_Service $token_service ) { self::$token_service = $token_service; } /** * Sets the customer service instance. This is needed only for tests. * * @param WC_Payments_Customer_Service $customer_service_class Instance of WC_Payments_Customer_Service. * * @return void */ public static function set_customer_service( WC_Payments_Customer_Service $customer_service_class ) { self::$customer_service = $customer_service_class; } /** * Returns the WC_Payments_Session_Service instance * * @return WC_Payments_Session_Service Session Service instance */ public static function get_session_service() { return self::$session_service; } /** * Registers the payment method with the blocks registry. * * @param Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry $payment_method_registry The registry. */ public static function register_checkout_gateway( $payment_method_registry ) { require_once __DIR__ . '/class-wc-payments-blocks-payment-method.php'; $payment_method_registry->register( new WC_Payments_Blocks_Payment_Method() ); } /** * Handles upgrade routines. */ public static function install_actions() { if ( version_compare( WCPAY_VERSION_NUMBER, get_option( 'woocommerce_woocommerce_payments_version' ), '>' ) ) { do_action( 'woocommerce_woocommerce_payments_updated' ); self::update_plugin_version(); } } /** * Updates the plugin version in db. */ public static function update_plugin_version() { update_option( 'woocommerce_woocommerce_payments_version', WCPAY_VERSION_NUMBER ); } /** * Sets the plugin activation timestamp. * * Use add_option so that we don't overwrite the value. */ public static function set_plugin_activation_timestamp() { add_option( 'wcpay_activation_timestamp', time() ); } /** * Adds WCPay notes to the WC-Admin inbox. */ public static function add_woo_admin_notes() { // Do not try to add notes on ajax requests to improve their performance. if ( wp_doing_ajax() ) { return; } if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) { require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-set-https-for-checkout.php'; WC_Payments_Notes_Set_Https_For_Checkout::possibly_add_note(); require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-set-up-stripelink.php'; WC_Payments_Notes_Set_Up_StripeLink::set_gateway( self::get_gateway() ); WC_Payments_Notes_Set_Up_StripeLink::possibly_add_note(); require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-stripe-billing-deprecation.php'; WC_Payments_Notes_Stripe_Billing_Deprecation::possibly_add_note(); require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-canceled-auth-remediation.php'; WC_Payments_Notes_Canceled_Auth_Remediation::possibly_add_note(); } if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '7.5', '<' ) && get_woocommerce_currency() === 'NOK' ) { add_filter( 'admin_notices', [ __CLASS__, 'wcpay_show_old_woocommerce_for_norway_notice' ] ); } add_filter( 'admin_notices', [ __CLASS__, 'wcpay_show_old_woocommerce_for_hungary_sweden_and_czech_republic' ] ); } /** * Removes deprecated notes i.e. no longer required notes. * * @return void */ public static function remove_deprecated_notes() { if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) { require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-qualitative-feedback.php'; WC_Payments_Notes_Qualitative_Feedback::possibly_delete_note(); } } /** * Shows an alert notice for Norwegian merchants on WooCommerce 7.4 and below */ public static function wcpay_show_old_woocommerce_for_norway_notice() { ?> <div class="notice wcpay-notice notice-error"> <p> <?php echo WC_Payments_Utils::esc_interpolated_html( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped sprintf( /* translators: %1$s: WooCommerce, %2$s: WooPayments, a1: documentation URL */ __( 'The %1$s version you have installed is not compatible with %2$s for a Norwegian business. Please update %1$s to version 7.5 or above. You can do that via the <a1>the plugins page.</a1>', 'woocommerce-payments' ), 'WooCommerce', 'WooPayments' ), [ 'a1' => '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', ] ) ?> </p> </div> <?php } /** * Removes WCPay notes from the WC-Admin inbox. */ public static function remove_woo_admin_notes() { if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) { self::$remote_note_service->delete_notes(); require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-qualitative-feedback.php'; WC_Payments_Notes_Qualitative_Feedback::possibly_delete_note(); require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-set-https-for-checkout.php'; WC_Payments_Notes_Set_Https_For_Checkout::possibly_delete_note(); require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-instant-deposits-eligible.php'; WC_Payments_Notes_Instant_Deposits_Eligible::possibly_delete_note(); require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-set-up-stripelink.php'; WC_Payments_Notes_Set_Up_StripeLink::possibly_delete_note(); require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-stripe-billing-deprecation.php'; WC_Payments_Notes_Stripe_Billing_Deprecation::possibly_delete_note(); require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-canceled-auth-remediation.php'; WC_Payments_Notes_Canceled_Auth_Remediation::possibly_delete_note(); } } /** * Filter to check if WCPay should operate as usual (the customer can save payment methods at checkout and those payment methods * will only be used on this site), or if saved cards should be available for all the sites on the multisite network. * * NOTE: DON'T USE THIS FILTER. Everything will break. At this moment, it's only intended to be used internally by Automattic. * * @return bool Normal WCPay behavior (false, default) or TRUE if the site should only use network-wide saved payment methods. */ public static function is_network_saved_cards_enabled() { return apply_filters( 'wcpay_force_network_saved_cards', false ); } /** * Registers woopay hooks if the woopay feature flag is enabled. * Removes WooPay webhooks if the merchant is not eligible. * * @return void */ public static function maybe_register_woopay_hooks() { $is_woopay_eligible = WC_Payments_Features::is_woopay_eligible(); // Feature flag. $is_woopay_enabled = 'yes' === self::get_gateway()->get_option( 'platform_checkout', 'no' ); if ( $is_woopay_eligible && $is_woopay_enabled ) { add_action( 'wc_ajax_wcpay_init_woopay', [ WooPay_Session::class, 'ajax_init_woopay' ] ); add_action( 'wc_ajax_wcpay_get_woopay_session', [ WooPay_Session::class, 'ajax_get_woopay_session' ] ); add_action( 'wc_ajax_wcpay_set_woopay_phone_number', [ WooPay_Session::class, 'ajax_set_woopay_phone_number' ] ); add_action( 'wc_ajax_wcpay_get_woopay_signature', [ __CLASS__, 'ajax_get_woopay_signature' ] ); add_action( 'wc_ajax_wcpay_get_woopay_minimum_session_data', [ WooPay_Session::class, 'ajax_get_woopay_minimum_session_data' ] ); // This injects the payments API and draft orders into core, so the WooCommerce Blocks plugin is not necessary. // We should remove this once both features are available by default in the WC minimum supported version. // - The payments API is currently only available in feature builds (with flag `WC_BLOCKS_IS_FEATURE_PLUGIN`). // - The Draft order status is available after WC blocks 7.5.0. if ( ! defined( 'WC_BLOCKS_IS_FEATURE_PLUGIN' ) && class_exists( 'Automattic\WooCommerce\Blocks\Package' ) && class_exists( 'Automattic\WooCommerce\Blocks\Payments\Api' ) ) { // Register payments API. $blocks_package_container = Automattic\WooCommerce\Blocks\Package::container(); $blocks_package_container->register( Automattic\WooCommerce\Blocks\Payments\Api::class, function ( $container ) { $payment_method_registry = $container->get( Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry::class ); $asset_data_registry = $container->get( Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::class ); return new Automattic\WooCommerce\Blocks\Payments\Api( $payment_method_registry, $asset_data_registry ); } ); $blocks_package_container->get( Automattic\WooCommerce\Blocks\Payments\Api::class ); // Register draft orders. $draft_orders = $blocks_package_container->get( Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders::class ); add_filter( 'wc_order_statuses', [ $draft_orders, 'register_draft_order_status' ] ); add_filter( 'woocommerce_register_shop_order_post_statuses', [ $draft_orders, 'register_draft_order_post_status' ] ); add_filter( 'woocommerce_analytics_excluded_order_statuses', [ $draft_orders, 'append_draft_order_post_status' ] ); add_filter( 'woocommerce_valid_order_statuses_for_payment', [ $draft_orders, 'append_draft_order_post_status' ] ); add_filter( 'woocommerce_valid_order_statuses_for_payment_complete', [ $draft_orders, 'append_draft_order_post_status' ] ); // Hook into the query to retrieve My Account orders so draft status is excluded. add_action( 'woocommerce_my_account_my_orders_query', [ $draft_orders, 'delete_draft_order_post_status_from_args' ] ); add_action( 'woocommerce_cleanup_draft_orders', [ $draft_orders, 'delete_expired_draft_orders' ] ); add_action( 'admin_init', [ $draft_orders, 'install' ] ); } new WooPay_Order_Status_Sync( self::$api_client, self::$account ); } } /** * Initializes woopay direct checkout if the woopay feature flag is enabled. * * @return void */ public static function maybe_init_woopay_direct_checkout() { if ( ! WC_Payments_Features::is_woopay_direct_checkout_enabled() ) { return; } $woopay_direct_checkout = new WC_Payments_WooPay_Direct_Checkout( self::$woopay_util ); $woopay_direct_checkout->init(); } /** * Validates whether the common config script should be enqueued and enqueues it. * * If the express checkout button is disabled on the cart page, the common config * script needs to be enqueued to ensure `wcpayConfig` is available on the cart page. * * @return void */ public static function validate_and_enqueue_woopay_common_config_script() { $is_express_button_disabled_on_cart = self::get_express_checkout_helper()->is_cart() && ! self::get_express_checkout_helper()->is_express_checkout_method_enabled_at( 'cart', 'woopay' ); if ( $is_express_button_disabled_on_cart ) { self::enqueue_woopay_common_config_script(); } } /** * Enqueues the common config script. * Only enqueues if the full config hasn't already been loaded via express button scripts. * * @return void */ public static function enqueue_woopay_common_config_script() { // Skip if the express button script is already enqueued/registered, as it includes the full config. if ( wp_script_is( 'WCPAY_WOOPAY_EXPRESS_BUTTON', 'enqueued' ) || wp_script_is( 'WCPAY_WOOPAY_EXPRESS_BUTTON', 'registered' ) ) { return; } try { // is_test() throws if the class 'Mode' has not been initialized. $is_test_mode = self::mode()->is_test(); } catch ( Exception $e ) { // Default to false if the class 'Mode' has not been initialized. $is_test_mode = false; } $common_config = [ 'woopayHost' => WooPay_Utilities::get_woopay_url(), 'testMode' => $is_test_mode, 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'woopaySessionNonce' => wp_create_nonce( 'woopay_session_nonce' ), 'woopayMerchantId' => Jetpack_Options::get_option( 'id' ), 'isWooPayDirectCheckoutEnabled' => WC_Payments_Features::is_woopay_direct_checkout_enabled(), 'platformTrackerNonce' => wp_create_nonce( 'platform_tracks_nonce' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'woopayMinimumSessionData' => WooPay_Session::get_woopay_minimum_session_data(), ]; wp_register_script( 'WCPAY_WOOPAY_COMMON_CONFIG', '', [], WCPAY_VERSION_NUMBER, false ); wp_add_inline_script( 'WCPAY_WOOPAY_COMMON_CONFIG', 'var wcpayConfig = Object.assign( wcpayConfig || {}, ' . wp_json_encode( $common_config ) . ' );', 'before' ); wp_enqueue_script( 'WCPAY_WOOPAY_COMMON_CONFIG' ); } /** * Enqueues the common config script if the WooPay Direct Checkout flow is * enabled and the express checkout button is disabled on the cart page. * * @param bool $should_enqueue Whether the script should be enqueued. * * @return void */ public static function maybe_enqueue_woopay_common_config_script( $should_enqueue ) { if ( ! $should_enqueue ) { return; } add_action( 'wp_enqueue_scripts', [ __CLASS__, 'validate_and_enqueue_woopay_common_config_script' ] ); } /** * Initializes express checkout buttons if payments are enabled * * @return void */ public static function maybe_display_express_checkout_buttons() { if ( WC_Payments_Features::are_payments_enabled() ) { $woopay_button_handler = new WC_Payments_WooPay_Button_Handler( self::$account, self::get_gateway(), self::$woopay_util, self::get_express_checkout_helper() ); $express_checkout_ajax_handler = new WC_Payments_Express_Checkout_Ajax_Handler( self::get_express_checkout_helper() ); $express_checkout_element_button_handler = new WC_Payments_Express_Checkout_Button_Handler( self::$account, self::get_gateway(), self::get_express_checkout_helper(), $express_checkout_ajax_handler ); $express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler( self::get_gateway(), $woopay_button_handler, $express_checkout_element_button_handler, $express_checkout_ajax_handler, self::get_express_checkout_helper() ); $express_checkout_button_display_handler->init(); } } /** * Retrieve a woopay request signature. * * @return void */ public static function ajax_get_woopay_signature() { $is_nonce_valid = check_ajax_referer( 'woopay_signature_nonce', false, false ); if ( ! $is_nonce_valid ) { wp_send_json_error( __( 'You aren’t authorized to do that.', 'woocommerce-payments' ), 403 ); } $woopay_util = new WooPay_Utilities(); $signature = $woopay_util->get_woopay_request_signature(); wp_send_json_success( [ 'signature' => $signature, ], 200 ); } /** * Get cart total. */ public static function ajax_get_cart_total() { check_ajax_referer( 'wcpay-get-cart-total', 'security' ); if ( ! defined( 'WOOCOMMERCE_CART' ) ) { define( 'WOOCOMMERCE_CART', true ); } if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { define( 'WOOCOMMERCE_CHECKOUT', true ); } WC()->cart->calculate_totals(); $cart_total = WC()->cart->total; $currency_code = get_woocommerce_currency(); wp_send_json( [ 'total' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ) ] ); } /** * Check if BNPL is available for the given price, currency, and country. */ public static function ajax_check_bnpl_availability() { check_ajax_referer( 'wcpay-is-bnpl-available', 'security' ); $price = floatval( $_POST['price'] ); $currency = sanitize_text_field( wp_unslash( $_POST['currency'] ) ); $country = sanitize_text_field( wp_unslash( $_POST['country'] ) ); $enabled_bnpl_payment_methods = array_intersect( Payment_Method::BNPL_PAYMENT_METHODS, self::get_gateway()->get_upe_enabled_payment_method_ids() ); $is_available = WC_Payments_Utils::is_any_bnpl_method_available( $enabled_bnpl_payment_methods, $country, $currency, $price ); wp_send_json_success( [ 'is_available' => $is_available ] ); } /** * Adds custom email field. */ public static function woopay_fields_before_billing_details() { $checkout = WC()->checkout; echo '<div class="woocommerce-billing-fields" id="contact_details">'; echo '<h3>' . esc_html( __( 'Contact information', 'woocommerce-payments' ) ) . '</h3>'; echo '<div class="woocommerce-billing-fields__field-wrapper">'; woocommerce_form_field( 'billing_email', [ 'type' => 'email', 'label' => __( 'Email address', 'woocommerce-payments' ), 'class' => [ 'form-row-wide woopay-billing-email' ], 'input_class' => [ 'woopay-billing-email-input' ], 'validate' => [ 'email' ], 'required' => true, ], $checkout->get_value( 'billing_email' ) ); echo '</div>'; echo '</div>'; // Ensure WC Blocks styles are enqueued so the spinner will show. // This style is not enqueued be default when using a block theme and classic checkout. wp_enqueue_style( 'wc-blocks-style' ); } /** * Hide the core email field * * @param string $field The checkout field being filtered. * @param string $key The field key. * @param mixed $args Field arguments. * @param string $value Field value. * @return string */ public static function filter_woocommerce_form_field_woopay_email( $field, $key, $args, $value ) { $class = $args['class'][0]; if ( false === strpos( $class, 'woopay-billing-email' ) && is_checkout() && ! is_checkout_pay_page() ) { $field = ''; } return $field; } /** * Enqueue cart page scripts. * * @return void */ public static function enqueue_cart_scripts() { if ( ! WC_Payments_Utils::is_cart_page() || ! self::get_gateway()->is_enabled() ) { return; } self::register_script_with_dependencies( 'WCPAY_CART', 'dist/cart' ); wp_enqueue_script( 'WCPAY_CART' ); if ( WC_Payments_Utils::is_cart_block() ) { self::register_script_with_dependencies( 'WCPAY_CART_BLOCK', 'dist/cart-block', [ 'wc-cart-block-frontend' ] ); wp_enqueue_script( 'WCPAY_CART_BLOCK' ); // Enqueue cart block styles. WC_Payments_Utils::enqueue_style( 'WCPAY_CART_BLOCK', plugins_url( 'dist/cart-block.css', WCPAY_PLUGIN_FILE ), [], self::get_file_version( 'dist/cart-block.css' ), 'all' ); } } /** * Register woopay hooks and scripts if feature is available. * * @return void */ public static function init_woopay() { // Load woopay save user section if feature is enabled. if ( self::$woopay_util->should_enable_woopay( self::get_gateway() ) ) { // Update email field location. add_action( 'woocommerce_checkout_billing', [ __CLASS__, 'woopay_fields_before_billing_details' ], -50 ); add_filter( 'woocommerce_form_field_email', [ __CLASS__, 'filter_woocommerce_form_field_woopay_email' ], 20, 4 ); add_action( 'woocommerce_checkout_process', [ __CLASS__, 'maybe_show_woopay_phone_number_error' ] ); include_once __DIR__ . '/woopay-user/class-woopay-save-user.php'; new WooPay_Save_User(); } } /** * Load stripe site messaging script. * * @return void */ public static function load_stripe_bnpl_site_messaging() { // The messaging element shall not be shown for subscription products. // As we are not too deep into subscriptions API, we follow simplistic approach for now. $is_subscription = false; $cart_contains_subscription = false; $are_subscriptions_enabled = class_exists( 'WC_Subscriptions' ) || class_exists( 'WC_Subscriptions_Core_Plugin' ); if ( $are_subscriptions_enabled ) { global $product; $is_subscription = $product && WC_Subscriptions_Product::is_subscription( $product ); $cart_contains_subscription = is_cart() && WC_Subscriptions_Cart::cart_contains_subscription(); } if ( ! $is_subscription && ! $cart_contains_subscription ) { require_once __DIR__ . '/class-wc-payments-payment-method-messaging-element.php'; $stripe_site_messaging = new WC_Payments_Payment_Method_Messaging_Element( self::$account, self::$card_gateway ); echo wp_kses( $stripe_site_messaging->init() ?? '', 'post' ); } } /** * Load webpack runtime script, only if SCRIPT_DEBUG is enabled and the script exists. * Required for webpack server with HMR. * * @return void */ public static function enqueue_dev_runtime_scripts() { if ( ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) && file_exists( WCPAY_ABSPATH . 'dist/runtime.js' ) ) { wp_enqueue_script( 'WCPAY_RUNTIME', plugins_url( 'dist/runtime.js', WCPAY_PLUGIN_FILE ), [], self::get_file_version( 'dist/runtime.js' ), true ); } } /** * Creates a new request object for a server call. * * @param string $class_name The name of the request class. Must extend WCPay\Core\Server\Request. * @param mixed $id The item ID, if the request needs it (Optional). * @return Request * @throws Exception If the request class is not really a request. */ public static function create_request( $class_name, $id = null ) { /** * Used for unit tests only, as requests have dependencies, which are not publicly available in live mode. * * @param Request $request Null, but if the filter returns a request, it will be used. * @param string $class_name The name of the request class. */ $request = apply_filters( 'wcpay_create_request', null, $class_name, $id ); if ( $request instanceof Request ) { return $request; } if ( ! is_subclass_of( $class_name, Request::class ) ) { throw new Exception( sprintf( 'WC_Payments::create_request() requires a class, which extends %s, %s provided instead', Request::class, $class_name ) ); } return new $class_name( self::get_payments_api_client(), self::get_wc_payments_http(), $id ); } /** * Inject an inline script with WCPay assets properties. * window.wcpayAssets.url – Dist URL, required to properly load chunks on sites with JS concatenation enabled. * * @return void */ public static function enqueue_assets_script() { wp_register_script( 'WCPAY_ASSETS', '', [], WCPAY_VERSION_NUMBER, false ); wp_enqueue_script( 'WCPAY_ASSETS' ); wp_localize_script( 'WCPAY_ASSETS', 'wcpayAssets', [ 'url' => plugins_url( '/dist/', WCPAY_PLUGIN_FILE ), ] ); } /** * Shows an alert notice for Hungarian, Sweden, and Czech Republic merchants on WooCommerce 7.4 and below */ public static function wcpay_show_old_woocommerce_for_hungary_sweden_and_czech_republic() { $currencies = [ 'HUF', 'SEK', 'CZK' ]; $store_currency = get_woocommerce_currency(); $should_show_error = in_array( $store_currency, $currencies, true ); if ( ! defined( 'WC_VERSION' ) || ! version_compare( WC_VERSION, '7.8', '<' ) || ! $should_show_error ) { return; } $notice = ''; switch ( $store_currency ) { case 'HUF': /* translators: %1$s: WooCommerce , %2$s: WooPayments, %3$s: The current WooCommerce version used by the store */ $notice = __( 'The %1$s version you have installed is not compatible with %2$s for a Hungarian business. Please update %1$s to version 7.8 or above (you are using %3$s). You can do that via the <a1>the plugins page.</a1>', 'woocommerce-payments' ); break; case 'SEK': /* translators: %1$s: WooCommerce , %2$s: WooPayments, %3$s: The current WooCommerce version used by the store */ $notice = __( 'The %1$s version you have installed is not compatible with %2$s for a Swedish business. Please update %1$s to version 7.8 or above (you are using %3$s). You can do that via the <a1>the plugins page.</a1>', 'woocommerce-payments' ); break; case 'CZK': /* translators: %1$s: WooCommerce , %2$s: WooPayments, %3$s: The current WooCommerce version used by the store */ $notice = __( 'The %1$s version you have installed is not compatible with %2$s for a Czech Republic business. Please update %1$s to version 7.8 or above (you are using %3$s). You can do that via the <a1>the plugins page.</a1>', 'woocommerce-payments' ); break; } ?> <div class="notice wcpay-notice notice-error"> <p> <?php echo WC_Payments_Utils::esc_interpolated_html( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped sprintf( $notice, 'WooCommerce', 'WooPayments', esc_html( WC_VERSION ) ), [ 'a1' => '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', ] ) ?> </p> </div> <?php } /** * Disable the WCPay Subscriptions feature on WooPayments plugin update if it's enabled and the store is no longer eligible. * * @see WC_Payments_Features::is_wcpay_subscriptions_eligible() for eligibility criteria. */ public static function maybe_disable_wcpay_subscriptions_on_update() { if ( WC_Payments_Features::is_wcpay_subscriptions_enabled() && ( class_exists( 'WC_Subscriptions' ) || ! WC_Payments_Features::is_wcpay_subscriptions_eligible() ) ) { update_option( WC_Payments_Features::WCPAY_SUBSCRIPTIONS_FLAG_NAME, '0' ); } } /** * Show error when WooPay opt-in is checked but no phone number was typed. */ public static function maybe_show_woopay_phone_number_error() { // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( isset( $_POST['save_user_in_woopay'] ) && 'true' === $_POST['save_user_in_woopay'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! isset( $_POST['woopay_user_phone_field'] ) || ! isset( $_POST['woopay_user_phone_field']['no-country-code'] ) || empty( $_POST['woopay_user_phone_field']['no-country-code'] ) ) { wc_add_notice( '<strong>' . __( 'Mobile Number', 'woocommerce-payments' ) . '</strong> ' . __( 'is required to create an WooPay account.', 'woocommerce-payments' ), 'error' ); } } } /** * Removes the Stripe Billing deprecation note when WooCommerce Subscriptions is installed. * * @param string $plugin The plugin being activated. */ public static function maybe_remove_stripe_billing_deprecation_note( $plugin ) { if ( 'woocommerce-subscriptions/woocommerce-subscriptions.php' === $plugin || 'woocommerce-com-woocommerce-subscriptions/woocommerce-subscriptions.php' === $plugin ) { require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-stripe-billing-deprecation.php'; WC_Payments_Notes_Stripe_Billing_Deprecation::possibly_delete_note(); } } /** * Update the Stripe Billing deprecation note. */ public static function maybe_update_stripe_billing_deprecation_note() { // If bundled subscriptions are not enabled or WooCommerce Subscriptions is active, do not update the note. $has_bundled_subs = WC_Payments_Features::is_wcpay_subscriptions_enabled() || WC_Payments_Features::is_stripe_billing_enabled(); if ( ! $has_bundled_subs || class_exists( 'WC_Subscriptions' ) ) { return; } require_once WCPAY_ABSPATH . 'includes/notes/class-wc-payments-notes-stripe-billing-deprecation.php'; WC_Payments_Notes_Stripe_Billing_Deprecation::possibly_update_note(); } /** * Create the HTTP instantiation. * * @return WC_Payments_Http_Interface */ private static function get_wc_payments_http() { require_once __DIR__ . '/wc-payment-api/class-wc-payments-http-interface.php'; require_once __DIR__ . '/wc-payment-api/class-wc-payments-http.php'; $http_class = apply_filters( 'wc_payments_http', null ); if ( ! $http_class instanceof WC_Payments_Http_Interface ) { $http_class = new WC_Payments_Http( new Automattic\Jetpack\Connection\Manager( 'woocommerce-payments' ) ); $http_class->init_hooks(); } return $http_class; } /** * Determines whether we should load Stripe Billing integration classes. * * Return true when: * - the WCPay Subscriptions feature is enabled & the Woo Subscriptions plugin isn't active, or * - Woo Subscriptions plugin is active and Stripe Billing is enabled or there are Stripe Billing Subscriptions. * * @see WC_Payments_Features::should_use_stripe_billing() * * @return bool */ private static function should_load_stripe_billing_integration() { if ( WC_Payments_Features::should_use_stripe_billing() ) { return true; } if ( ! function_exists( 'wcs_get_orders_with_meta_query' ) ) { return false; } // If there are any Stripe Billing Subscriptions, we should load the Stripe Billing integration classes. eg while a migration is in progress, or to support legacy subscriptions. $result = wcs_get_orders_with_meta_query( [ 'status' => 'any', 'return' => 'ids', 'type' => 'shop_subscription', 'limit' => 1, // We only need to know if there are any - at least 1. 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query [ 'key' => '_wcpay_subscription_id', 'compare' => 'EXISTS', ], ], ] ); return (bool) ( is_countable( $result ) ? count( $result ) : 0 ); } } includes/exceptions/class-invalid-payment-method-exception.php 0000777 00000000471 15251156605 0020741 0 ustar 00 <?php /** * Class Invalid_Payment_Method_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when payment method can not be identified or used. */ class Invalid_Payment_Method_Exception extends Base_Exception { } includes/exceptions/class-invalid-phone-number-exception.php 0000777 00000000453 15251156605 0020405 0 ustar 00 <?php /** * Class Invalid_Phone_Number_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when phone number is invalid. */ class Invalid_Phone_Number_Exception extends Process_Payment_Exception { } includes/exceptions/class-invalid-price-exception.php 0000777 00000000506 15251156605 0017107 0 ustar 00 <?php /** * Class Rest_Request_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; use Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing errors in REST API controllers (e.g. issues with missing parameters in requests). */ class Invalid_Price_Exception extends Exception {} includes/exceptions/class-rest-request-exception.php 0000777 00000000505 15251156605 0017023 0 ustar 00 <?php /** * Class Rest_Request_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; use Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing errors in REST API controllers (e.g. issues with missing parameters in requests). */ class Rest_Request_Exception extends Exception {} includes/exceptions/class-cannot-combine-currencies-exception.php 0000777 00000002271 15251156605 0021416 0 ustar 00 <?php /** * Class Cannot_Combine_Currencies_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Class representing Amount_Too_Small_Exception */ class Cannot_Combine_Currencies_Exception extends API_Exception { /** * Holds the attempted currency, extracted from the error message returned by the API. * * @var string */ private $currency; /** * Constructor * * @param string $message The Exception message to throw. * @param string $currency The currency for which this is the minimum amount. * @param int $http_code HTTP response code. * @param int $code The Exception code. * @param \Throwable $previous The previous exception used for the exception chaining. */ public function __construct( $message, $currency, $http_code, $code = 0, $previous = null ) { $this->currency = $currency; parent::__construct( $message, 'cannot_combine_currencies', $http_code, null, null, $code, $previous ); } /** * Returns the currency of the minumum required amount. * * @return string */ public function get_currency() { return $this->currency; } } includes/exceptions/class-fraud-prevention-enabled-exception.php 0000777 00000000477 15251156605 0021250 0 ustar 00 <?php /** * Class Fraud_Prevention_Enabled_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when fraud prevension service is enabled. */ class Fraud_Prevention_Enabled_Exception extends Process_Payment_Exception { } includes/exceptions/class-amount-too-large-exception.php 0000777 00000001047 15251156605 0017554 0 ustar 00 <?php /** * Class Amount_Too_Large_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Class representing Amount_Too_Large_Exception */ class Amount_Too_Large_Exception extends API_Exception { /** * Constructor * * @param string $message The Exception message to throw. * @param int $http_code HTTP response code. */ public function __construct( $message, $http_code ) { parent::__construct( $message, 'amount_too_large', $http_code, null, null ); } } includes/exceptions/class-add-payment-method-exception.php 0000777 00000000550 15251156605 0020041 0 ustar 00 <?php /** * Class Add_Payment_Method_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when we have issues with creating * payment methods, e.g. invalid requests or errors on the server side. */ class Add_Payment_Method_Exception extends Base_Exception { } includes/exceptions/class-order-id-mismatch-exception.php 0000777 00000000456 15251156605 0017675 0 ustar 00 <?php /** * Class Order_ID_Mismatch_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when payment processing is not possible or fails. */ class Order_ID_Mismatch_Exception extends Base_Exception { } includes/exceptions/class-fraud-ruleset-exception.php 0000777 00000000447 15251156605 0017147 0 ustar 00 <?php /** * Class Fraud_Ruleset_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; use Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing errors in the fraud prevention and ruleset logic. */ class Fraud_Ruleset_Exception extends Exception {} includes/exceptions/class-subscription-mode-mismatch-exception.php 0000777 00000001434 15251156605 0021633 0 ustar 00 <?php /** * Class Subscription_Mode_Mismatch_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; use WCPay\Exceptions\Base_Exception; defined( 'ABSPATH' ) || exit; /** * Subscription_Mode_Mismatch_Exception class. */ class Subscription_Mode_Mismatch_Exception extends Base_Exception { /** * Subscription_Mode_Mismatch_Exception constructor. * * @param string $message The exception message. * @param int $code The exception code. * @param \Throwable|null $previous The previous exception. */ public function __construct( $message = 'The subscription mode does not match the current WooPayments mode.', $code = 0, $previous = null ) { parent::__construct( $message, 'subscription_mode_mismatch', $code, $previous ); } } includes/exceptions/class-process-payment-exception.php 0000777 00000000452 15251156605 0017512 0 ustar 00 <?php /** * Class Process_Payment_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when payment processing is not possible or fails. */ class Process_Payment_Exception extends Base_Exception { } includes/exceptions/class-invalid-address-exception.php 0000777 00000000431 15251156605 0017427 0 ustar 00 <?php /** * Class Invalid_Address_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; use Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing errors when address is invalid. */ class Invalid_Address_Exception extends Exception {} includes/exceptions/class-connection-exception.php 0000777 00000000365 15251156605 0016523 0 ustar 00 <?php /** * Class Connection_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Class representing Connection_Exception */ class Connection_Exception extends API_Exception { } includes/exceptions/class-base-exception.php 0000777 00000002237 15251156605 0015276 0 ustar 00 <?php /** * Class Base_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; use Exception; defined( 'ABSPATH' ) || exit; /** * Abstract class for payments extension exceptions, where we allow to inject * human-friendly error codes, e.g. 'order_not_found'. */ abstract class Base_Exception extends Exception { /** * String error code, for example 'order_not_found'. * * @var string */ private $error_code; /** * Constructor, including the usual $message, $code, and $previous, * and a new parameter $error_code. * * @param string $message The Exception message to throw. * @param string $error_code String error code. * @param int $code The Exception code. * @param \Throwable $previous The previous exception used for the exception chaining. */ public function __construct( $message, $error_code, $code = 0, $previous = null ) { $this->error_code = $error_code; parent::__construct( $message, $code, $previous ); } /** * Returns the error code. * * @return string Error code, for example 'order_not_found'. */ public function get_error_code() { return $this->error_code; } } includes/exceptions/class-api-merchant-exception.php 0000777 00000003057 15251156605 0016735 0 ustar 00 <?php /** * Class API_Merchant_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Class extending API_Exception to include the error message for merchants only. */ class API_Merchant_Exception extends API_Exception { /** * Merchant message. This message should not be shown to shoppers. * * @var string */ private $merchant_message; /** * Constructor * * @param string $message The Exception message to throw. * @param string $error_code Error code returned by the server, for example wcpay_account_not_found. * @param int $http_code HTTP response code. * @param string $merchant_message The merchant message. This message should not be shown to shoppers. * @param string|null $error_type Error type attribute. * @param string|null $decline_code The decline code if it is a card error. * @param int $code The Exception code. * @param \Throwable|null $previous The previous exception used for the exception chaining. */ public function __construct( $message, $error_code, $http_code, $merchant_message, $error_type = null, $decline_code = null, $code = 0, $previous = null ) { $this->merchant_message = $merchant_message; parent::__construct( $message, $error_code, $http_code, $error_type, $decline_code, $code, $previous ); } /** * Returns the merchant message. * * @return string Merchant message. */ public function get_merchant_message(): string { return $this->merchant_message; } } includes/exceptions/class-amount-too-small-exception.php 0000777 00000003012 15251156605 0017564 0 ustar 00 <?php /** * Class Amount_Too_Small_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Class representing Amount_Too_Small_Exception */ class Amount_Too_Small_Exception extends API_Exception { /** * Holds the minimum amount, returned from the API. * * @var int */ private $amount; /** * Holds the currency for this minimum amount. * * @var string */ private $currency; /** * Constructor * * @param string $message The Exception message to throw. * @param int $minimum_amount The minimum amount, required to create an intent in the request currency. * @param string $currency The currency for which this is the minimum amount. * @param int $http_code HTTP response code. * @param int $code The Exception code. * @param \Throwable $previous The previous exception used for the exception chaining. */ public function __construct( $message, $minimum_amount, $currency, $http_code, $code = 0, $previous = null ) { $this->amount = $minimum_amount; $this->currency = $currency; parent::__construct( $message, 'amount_too_small', $http_code, null, null, $code, $previous ); } /** * Returns the minimum required amount. * * @return int */ public function get_minimum_amount() { return $this->amount; } /** * Returns the currency of the minumum required amount. * * @return string */ public function get_currency() { return $this->currency; } } includes/exceptions/class-invalid-webhook-data-exception.php 0000777 00000000453 15251156605 0020353 0 ustar 00 <?php /** * Class Invalid_Webhook_Data_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; use Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when webhook data is invalid. */ class Invalid_Webhook_Data_Exception extends Exception { } includes/exceptions/class-order-not-found-exception.php 0000777 00000000455 15251156605 0017406 0 ustar 00 <?php /** * Class Order_Not_Found_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when we have issues when trying to find/use an order. */ class Order_Not_Found_Exception extends Base_Exception {} includes/exceptions/class-intent-authentication-exception.php 0000777 00000000616 15251156605 0020701 0 ustar 00 <?php /** * Class Intent_Authentication_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when there's a problem updating an order after a payment * authentication attempt was made by the customer, e.g. for 3DS authentication. */ class Intent_Authentication_Exception extends Base_Exception { } includes/exceptions/class-api-exception.php 0000777 00000003672 15251156605 0015141 0 ustar 00 <?php /** * Class API_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Class representing API_Exception */ class API_Exception extends Base_Exception { /** * HTTP error code, for example 404, 500 etc. * * @var int */ private $http_code = 0; /** * Error type attribute from the server. * * @var string */ private $error_type = null; /** * Decline code if it is a card error. * * @var string */ private $decline_code = null; /** * Constructor * * @param string $message The Exception message to throw. * @param string $error_code Error code returned by the server, for example wcpay_account_not_found. * @param int $http_code HTTP response code. * @param string $error_type Error type attribute. * @param string $decline_code The decline code if it is a card error. * @param int $code The Exception code. * @param \Throwable $previous The previous exception used for the exception chaining. */ public function __construct( $message, $error_code, $http_code, $error_type = null, $decline_code = null, $code = 0, $previous = null ) { $this->http_code = $http_code; $this->error_type = $error_type; $this->decline_code = $decline_code; parent::__construct( $message, $error_code, $code, $previous ); } /** * Returns the HTTP code. * * @return int HTTP code, for example 404. */ public function get_http_code() { return $this->http_code; } /** * Returns the error type attribute from the server. * * @return string|null Error type, for example 'api_error' or 'card_error'. */ public function get_error_type() { return $this->error_type; } /** * Returns the decline code attribute from the server. * * @return string|null Decline code, for example 'expired_card' or 'insufficient_funds'. */ public function get_decline_code() { return $this->decline_code; } } includes/exceptions/class-rate-limiter-enabled-exception.php 0000777 00000000453 15251156605 0020350 0 ustar 00 <?php /** * Class Rate_Limiter_Enabled_Exception * * @package WooCommerce\Payments */ namespace WCPay\Exceptions; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when rate limiter is enabled. */ class Rate_Limiter_Enabled_Exception extends Process_Payment_Exception { } includes/class-wc-payments-token-service.php 0000777 00000047150 15251156605 0015235 0 ustar 00 <?php /** * WC_Payments_Token_Service class * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Logger; use WCPay\Constants\Payment_Method; /** * Handles and process WC payment tokens API. * Seen in checkout page and my account->add payment method page. */ class WC_Payments_Token_Service { const REUSABLE_GATEWAYS_BY_PAYMENT_METHOD = [ Payment_Method::CARD => WC_Payment_Gateway_WCPay::GATEWAY_ID, Payment_Method::SEPA => WC_Payment_Gateway_WCPay::GATEWAY_ID . '_' . Payment_Method::SEPA, Payment_Method::LINK => WC_Payment_Gateway_WCPay::GATEWAY_ID, ]; const CACHED_PAYMENT_METHODS_META_KEY = '_wcpay_payment_methods'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * WC_Payments_Customer instance for working with customer information * * @var WC_Payments_Customer_Service */ private $customer_service; /** * WC_Payments_Token_Service constructor. * * @param WC_Payments_API_Client $payments_api_client Payments API client. * @param WC_Payments_Customer_Service $customer_service Customer class instance. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Customer_Service $customer_service ) { $this->payments_api_client = $payments_api_client; $this->customer_service = $customer_service; } /** * Initializes hooks. */ public function init_hooks() { add_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 ); add_action( 'woocommerce_payment_token_set_default', [ $this, 'woocommerce_payment_token_set_default' ], 10, 2 ); add_filter( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 ); add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'get_account_saved_payment_methods_list_item_sepa' ], 10, 2 ); add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'get_account_saved_payment_methods_list_item_link' ], 10, 2 ); add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'get_account_saved_payment_methods_list_item_wallet' ], 10, 2 ); add_filter( 'woocommerce_get_credit_card_type_label', [ $this, 'normalize_sepa_label' ] ); add_filter( 'woocommerce_get_credit_card_type_label', [ $this, 'normalize_stripe_link_label' ] ); } /** * Creates and add a token to a user, based on the payment_method object * * @param array $payment_method Payment method to be added. * @param WP_User $user User to attach payment method to. * @return WC_Payment_Token The WC object for the payment token. */ public function add_token_to_user( $payment_method, $user ) { // Clear cached payment methods. $this->clear_cached_payment_methods_for_user( $user->ID ); switch ( $payment_method['type'] ) { case Payment_Method::SEPA: $token = new WC_Payment_Token_WCPay_SEPA(); $gateway_id = WC_Payment_Gateway_WCPay::GATEWAY_ID . '_' . Payment_Method::SEPA; $token->set_gateway_id( $gateway_id ); $token->set_last4( $payment_method[ Payment_Method::SEPA ]['last4'] ); break; case Payment_Method::LINK: $token = new WC_Payment_Token_WCPay_Link(); $gateway_id = WC_Payment_Gateway_WCPay::GATEWAY_ID; $token->set_gateway_id( $gateway_id ); $token->set_email( $payment_method[ Payment_Method::LINK ]['email'] ); break; case Payment_Method::CARD_PRESENT: $token = new WC_Payment_Token_CC(); $token->set_gateway_id( WC_Payment_Gateway_WCPay::GATEWAY_ID ); $token->set_expiry_month( $payment_method[ Payment_Method::CARD_PRESENT ]['exp_month'] ); $token->set_expiry_year( $payment_method[ Payment_Method::CARD_PRESENT ]['exp_year'] ); $token->set_card_type( strtolower( $payment_method[ Payment_Method::CARD_PRESENT ]['brand'] ) ); $token->set_last4( $payment_method[ Payment_Method::CARD_PRESENT ]['last4'] ); break; default: $token = new WC_Payment_Token_CC(); $token->set_gateway_id( WC_Payment_Gateway_WCPay::GATEWAY_ID ); $token->set_expiry_month( $payment_method[ Payment_Method::CARD ]['exp_month'] ); $token->set_expiry_year( $payment_method[ Payment_Method::CARD ]['exp_year'] ); $token->set_card_type( strtolower( $payment_method[ Payment_Method::CARD ]['display_brand'] ?? $payment_method[ Payment_Method::CARD ]['networks']['preferred'] ?? $payment_method[ Payment_Method::CARD ]['brand'] ) ); $token->set_last4( $payment_method[ Payment_Method::CARD ]['last4'] ); if ( ! empty( $payment_method[ Payment_Method::CARD ]['wallet']['type'] ) ) { $token->add_meta_data( '_wcpay_wallet_type', $payment_method[ Payment_Method::CARD ]['wallet']['type'], true ); } } $token->set_token( $payment_method['id'] ); $token->set_user_id( $user->ID ); $token->save(); return $token; } /** * Adds a payment method to a user. * * @param string $payment_method_id Payment method to be added. * @param WP_User $user User to attach payment method to. * @return WC_Payment_Token_CC The newly created token. */ public function add_payment_method_to_user( $payment_method_id, $user ) { $payment_method_object = $this->payments_api_client->get_payment_method( $payment_method_id ); return $this->add_token_to_user( $payment_method_object, $user ); } /** * Returns boolean value if payment method type matches relevant payment gateway. * * @param string $payment_method_type Stripe payment method type ID. * @param string $gateway_id WC payment gateway ID. * @return bool True, if payment method type matches gateway, false if otherwise. */ public function is_valid_payment_method_type_for_gateway( $payment_method_type, $gateway_id ) { return self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ $payment_method_type ] === $gateway_id; } /** * Clear payment methods cache for a user. * * @param int $user_id WC user ID. */ public function clear_cached_payment_methods_for_user( $user_id ) { if ( WC_Payments::is_network_saved_cards_enabled() ) { return; // No need to do anything, payment methods will never be cached in this case. } delete_user_meta( $user_id, self::CACHED_PAYMENT_METHODS_META_KEY ); } /** * Clear all cached payment methods. * Used when account data is updated and all payment method caches need to be cleared. */ public function clear_all_cached_payment_methods() { global $wpdb; if ( WC_Payments::is_network_saved_cards_enabled() ) { return; // No need to do anything, payment methods will never be cached in this case. } // Tap straight into the database and delete the meta key for all users. $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->usermeta WHERE meta_key = %s", self::CACHED_PAYMENT_METHODS_META_KEY ) ); /** * Legacy: Payment methods were stored in the database cache with the `wcpay_pm_` prefix. * When cleaning up cached payment methods, we need to flush the database from old cached data as well. * * This method gets called for account updates. Even though those are rare, they should be a * good opportunity to clean up old cached data. */ $options = $wpdb->get_results( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE 'wcpay_pm_%'" ); foreach ( $options as $option ) { delete_option( $option->option_name ); } } /** * Gets saved tokens from API if they don't already exist in WooCommerce. * * @param array $tokens Array of tokens. * @param string $user_id WC user ID. * @param string $gateway_id WC gateway ID. * @return array */ public function woocommerce_get_customer_payment_tokens( $tokens, $user_id, $gateway_id ) { if ( ( ! empty( $gateway_id ) && ! in_array( $gateway_id, self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD, true ) ) || ! is_user_logged_in() ) { return $tokens; } if ( count( $tokens ) >= get_option( 'posts_per_page' ) ) { // The tokens data store is not paginated and only the first "post_per_page" (defaults to 10) tokens are retrieved. // Having 10 saved credit cards is considered an unsupported edge case, new ones that have been stored in Stripe won't be added. return $tokens; } try { $customer_id = $this->customer_service->get_customer_id_by_user_id( $user_id ); if ( null === $customer_id ) { return $tokens; } $stored_tokens = []; foreach ( $tokens as $token ) { if ( in_array( $token->get_gateway_id(), self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD, true ) ) { $stored_tokens[ $token->get_token() ] = $token; } } $payment_methods = $this->get_payment_methods_from_stripe( $user_id, $customer_id, $gateway_id ); } catch ( Exception $e ) { Logger::error( 'Failed to fetch payment methods for customer.' . $e ); return $tokens; } // Prevent unnecessary recursion, WC_Payment_Token::save() ends up calling 'woocommerce_get_customer_payment_tokens' in some cases. remove_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 ); foreach ( $payment_methods as $payment_method ) { if ( ! isset( $payment_method['type'] ) ) { continue; } if ( ! isset( $stored_tokens[ $payment_method['id'] ] ) && ( $this->is_valid_payment_method_type_for_gateway( $payment_method['type'], $gateway_id ) || empty( $gateway_id ) ) ) { $token = $this->add_token_to_user( $payment_method, get_user_by( 'id', $user_id ) ); $tokens[ $token->get_id() ] = $token; } else { unset( $stored_tokens[ $payment_method['id'] ] ); } } add_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 ); // Remove the payment methods that no longer exist in Stripe's side. remove_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 ); foreach ( $stored_tokens as $token ) { unset( $tokens[ $token->get_id() ] ); $token->delete(); } add_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 ); return $tokens; } /** * Gets payment methods from Stripe. * * @param string $user_id WP user ID. * @param string $customer_id WC customer ID. * @param string $gateway_id WC gateway ID. * @return array Payment methods. */ private function get_payment_methods_from_stripe( $user_id, $customer_id, $gateway_id ) { // Prepare all payment method types that are to be retrieved, based on the gateway. $types_to_retrieve = $this->get_retrievable_payment_method_types( $gateway_id ); // Load cached data, verify it is for the same customer ID. Bust if they do not match. $cache = get_user_meta( $user_id, self::CACHED_PAYMENT_METHODS_META_KEY, true ); if ( ! is_array( $cache ) || ! isset( $cache['customer_id'] ) || $cache['customer_id'] !== $customer_id ) { $cache = [ 'customer_id' => $customer_id, ]; } $payment_methods = []; // Check whether all retrievable payment method types are cached. // Combine with existing data in case there are cached PMs for other gateway IDs. foreach ( $types_to_retrieve as $type ) { if ( isset( $cache[ 'payment_method_' . $type ] ) ) { $payment_methods = array_merge( $payment_methods, $cache[ 'payment_method_' . $type ] ); unset( $types_to_retrieve[ array_search( $type, $types_to_retrieve, true ) ] ); } } if ( empty( $types_to_retrieve ) ) { return $payment_methods; } foreach ( $types_to_retrieve as $type ) { $type_methods = $this->customer_service->get_payment_methods_for_customer( $customer_id, $type ); // Add to cache. $cache[ 'payment_method_' . $type ] = $type_methods; // Add to the list that will be returned. $payment_methods = array_merge( $payment_methods, $type_methods ); } update_user_meta( $user_id, self::CACHED_PAYMENT_METHODS_META_KEY, $cache ); return $payment_methods; } /** * Retrieves the payment method types for which tokens should be retrieved. * * This function determines the appropriate payment method types based on the provided gateway ID. * - If a gateway ID is provided, it retrieves the payment methods specific to that gateway to prevent duplication of saved tokens under incorrect payment methods during checkout. * - If no gateway ID is provided, it retrieves the default payment methods to fetch all saved tokens, e.g., for the Blocks checkout or My Account page. * * @param string|null $gateway_id The optional ID of the gateway. * @return array The list of retrievable payment method types. */ private function get_retrievable_payment_method_types( $gateway_id = null ) { if ( empty( $gateway_id ) ) { return $this->get_all_retrievable_payment_types(); } else { return $this->get_gateway_specific_retrievable_payment_types( $gateway_id ); } } /** * Returns all the enabled retrievable payment method types. * * @return array Enabled retrievable payment method types. */ private function get_all_retrievable_payment_types() { $types = [ Payment_Method::CARD ]; if ( $this->is_payment_method_enabled( Payment_Method::SEPA ) ) { $types[] = Payment_Method::SEPA; } if ( $this->is_payment_method_enabled( Payment_Method::LINK ) ) { $types[] = Payment_Method::LINK; } return $types; } /** * Returns retrievable payment method types for a given gateway. * * @param string $gateway_id The ID of the gateway. * @return array Retrievable payment method types for the specified gateway. */ private function get_gateway_specific_retrievable_payment_types( $gateway_id ) { $types = []; foreach ( self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD as $payment_method => $gateway ) { if ( $gateway !== $gateway_id ) { continue; } // Stripe Link is part of the card gateway, so we need to check separately if Link is enabled. if ( Payment_Method::LINK === $payment_method && ! $this->is_payment_method_enabled( Payment_Method::LINK ) ) { continue; } $types[] = $payment_method; } return $types; } /** * Checks if a payment method is enabled. * * @param string $payment_method The payment method to check. * @return bool True if the payment method is enabled, false otherwise. */ private function is_payment_method_enabled( $payment_method ) { return in_array( $payment_method, WC_Payments::get_gateway()->get_upe_enabled_payment_method_ids(), true ); } /** * Delete token from Stripe. * * @param string $token_id Token ID. * @param WC_Payment_Token $token Token object. * * @throws Exception */ public function woocommerce_payment_token_deleted( $token_id, $token ) { // If it's not reusable payment method, we don't need to perform any additional checks. if ( ! in_array( $token->get_gateway_id(), self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD, true ) ) { return; } // First check if it's live mode. // Second check if it's admin. // Third check if it's not production environment. // When all conditions are met, we don't want to delete the payment method from Stripe. // This is to avoid detaching the payment method from the live stripe account on non production environments. if ( WC_Payments::mode()->is_live() && is_admin() && 'production' !== wp_get_environment_type() ) { return; } try { $this->payments_api_client->detach_payment_method( $token->get_token() ); // Clear cached payment methods. $this->clear_cached_payment_methods_for_user( $token->get_user_id() ); } catch ( Exception $e ) { Logger::log( 'Error detaching payment method:' . $e->getMessage() ); } } /** * Set as default in Stripe. * * @param string $token_id Token ID. * @param WC_Payment_Token $token Token object. */ public function woocommerce_payment_token_set_default( $token_id, $token ) { if ( in_array( $token->get_gateway_id(), self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD, true ) ) { $customer_id = $this->customer_service->get_customer_id_by_user_id( $token->get_user_id() ); if ( $customer_id ) { $this->customer_service->set_default_payment_method_for_customer( $customer_id, $token->get_token() ); // Clear cached payment methods. $this->clear_cached_payment_methods_for_user( $token->get_user_id() ); } } } /** * Controls the output for SEPA on the my account page. * * @param array $item Individual list item from woocommerce_saved_payment_methods_list. * @param WC_Payment_Token|WC_Payment_Token_WCPay_SEPA $payment_token The payment token associated with this method entry. * @return array Filtered item */ public function get_account_saved_payment_methods_list_item_sepa( $item, $payment_token ) { if ( WC_Payment_Token_WCPay_SEPA::TYPE === strtolower( $payment_token->get_type() ) ) { $item['method']['last4'] = $payment_token->get_last4(); $item['method']['brand'] = esc_html__( 'SEPA IBAN', 'woocommerce-payments' ); } return $item; } /** * Controls the output for Stripe Link on the My account page. * * @param array $item Individual list item from woocommerce_saved_payment_methods_list. * @param WC_Payment_Token|WC_Payment_Token_WCPay_Link $payment_token The payment token associated with this method entry. * @return array Filtered item */ public function get_account_saved_payment_methods_list_item_link( $item, $payment_token ) { if ( WC_Payment_Token_WCPay_Link::TYPE === strtolower( $payment_token->get_type() ) ) { $item['method']['last4'] = $payment_token->get_redacted_email(); $item['method']['brand'] = esc_html__( 'Stripe Link email', 'woocommerce-payments' ); } return $item; } /** * Controls the output for Wallet tokens on the My account page. * * @param array $item Individual list item from woocommerce_saved_payment_methods_list. * @param WC_Payment_Token|WC_Payment_Token_WCPay_Link $payment_token The payment token associated with this method entry. * @return array Filtered item */ public function get_account_saved_payment_methods_list_item_wallet( $item, $payment_token ) { if ( 'cc' !== strtolower( $payment_token->get_type() ) ) { return $item; } $wallet_type = $payment_token->get_meta( '_wcpay_wallet_type', true ); if ( empty( $wallet_type ) ) { return $item; } // Google Pay and Apple Pay are separate payment methods, so we can retrieve them from the registered payment methods class. $payment_method = WC_Payments::get_payment_method_by_id( $wallet_type ); if ( ! $payment_method || ! method_exists( $payment_method, 'get_title' ) ) { return $item; } $original_brand = $item['method']['brand'] ?? ''; $item['method']['brand'] = sprintf( /* translators: 1: wallet name, 2: card brand */ _x( '%1$s %2$s', 'Payment token with wallet', 'woocommerce-payments' ), $payment_method->get_title(), $original_brand ); return $item; } /** * Normalizes the SEPA IBAN label on My Account page. * * @param string $label Token label. * @return string $label Capitalized SEPA IBAN label. */ public function normalize_sepa_label( $label ) { if ( 'sepa iban' === strtolower( $label ) ) { return __( 'SEPA IBAN', 'woocommerce-payments' ); } return $label; } /** * Normalizes the Stripe Link label on My Account page. * * @param string $label Token label. * @return string $label Capitalized SEPA IBAN label. */ public function normalize_stripe_link_label( $label ) { if ( 'stripe link email' === strtolower( $label ) ) { return 'Stripe Link email'; } return $label; } } includes/class-wc-payments-incentives-service.php 0000777 00000036315 15251156605 0016265 0 ustar 00 <?php /** * Class WC_Payments_Incentives_Service * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use Automattic\WooCommerce\Admin\WCAdminHelper; use WCPay\Database_Cache; /** * Class handling WooPayments incentives related business logic. */ class WC_Payments_Incentives_Service { const PREFIX = 'woocommerce_admin_pes_incentive_'; /** * The transient name for incentives cache. * * @var string */ private $cache_transient_name; /** * The transient name used to store the value for if store has orders. * * @var string */ private $store_has_orders_transient_name; /** * The option name used to store the value for if store had WooPayments in use. * * @var string */ private $store_had_woopayments_option_name; /** * The memoized incentives to avoid fetching multiple times during a request. * * @var array|null */ private $incentives_memo = null; /** * Cache util for managing onboarding data. * * @var Database_Cache */ private $database_cache; /** * Class constructor * * @param Database_Cache $database_cache Database cache util. */ public function __construct( Database_Cache $database_cache ) { $this->database_cache = $database_cache; // We use the same transient keys as the WC core suggestion incentives. // This way we can reuse the same cache for the incentives across the store admin. // @see \Automattic\WooCommerce\Internal\Admin\Suggestions\Incentives\WooPayments. $this->cache_transient_name = self::PREFIX . 'woopayments_cache'; $this->store_has_orders_transient_name = self::PREFIX . 'woopayments_store_has_orders'; $this->store_had_woopayments_option_name = self::PREFIX . 'woopayments_store_had_woopayments'; } /** * Initialise class hooks. * * @return void */ public function init_hooks() { add_action( 'admin_menu', [ $this, 'add_payments_menu_badge' ] ); add_filter( 'woocommerce_admin_allowed_promo_notes', [ $this, 'allowed_promo_notes' ] ); add_filter( 'woocommerce_admin_woopayments_onboarding_task_badge', [ $this, 'onboarding_task_badge' ] ); add_filter( 'woocommerce_admin_woopayments_onboarding_task_additional_data', [ $this, 'onboarding_task_additional_data' ], 20 ); } /** * Add badge to payments menu if there is an eligible incentive. * * @return void */ public function add_payments_menu_badge(): void { global $menu; // Return early if there is no eligible incentive. if ( ! $this->get_connect_incentive() ) { return; } $badge = WC_Payments_Admin::MENU_NOTIFICATION_BADGE; foreach ( $menu as $index => $menu_item ) { if ( false === strpos( $menu_item[0], $badge ) && ( 'wc-admin&path=/payments/connect' === $menu_item[2] ) ) { $menu[ $index ][0] .= $badge; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // One menu item with a badge is more than enough. break; } } } /** * Adds allowed promo notes from eligible incentive. * * @param array $promo_notes Current allowed promo notes. * @return array Updated allowed promo notes. */ public function allowed_promo_notes( $promo_notes = [] ): array { $incentive = $this->get_connect_incentive(); // Return early if there is no eligible incentive. if ( empty( $incentive['id'] ) ) { return $promo_notes; } $promo_notes[] = $incentive['id']; return $promo_notes; } /** * Adds the WooPayments incentive badge to the onboarding task. * * @param string $badge Current badge. * * @return string */ public function onboarding_task_badge( string $badge ): string { $incentive = $this->get_connect_incentive(); // Return early if there is no eligible incentive or there is no task badge for it. if ( empty( $incentive['task_badge'] ) ) { return $badge; } return $incentive['task_badge']; } /** * Filter the onboarding task additional data to add the WooPayments incentive data to it. * * @param ?array $additional_data The current task additional data. * * @return ?array The filtered task additional data. */ public function onboarding_task_additional_data( ?array $additional_data ): ?array { $incentive = $this->get_connect_incentive(); // Return early if there is no eligible incentive. if ( empty( $incentive['id'] ) ) { return $additional_data; } if ( empty( $additional_data ) ) { $additional_data = []; } $additional_data['wooPaymentsIncentiveId'] = $incentive['id']; return $additional_data; } /** * Clear the incentives cache. */ public function clear_cache() { delete_transient( $this->cache_transient_name ); $this->reset_memo(); } /** * Reset the memoized incentives. * * This is useful for testing purposes. */ public function reset_memo() { $this->incentives_memo = null; } /** * Get an eligible 'connect_page' type incentive, if available. * * @return array|null The incentive details or null if there is no eligible incentive. */ public function get_connect_incentive(): ?array { // Return early if there is an account connected. if ( WC_Payments::get_account_service()->is_stripe_connected() ) { return null; } // The country for which to get the incentive. $country_code = WC()->countries->get_base_country(); // Return early if the country is not supported. if ( ! array_key_exists( $country_code, WC_Payments_Utils::supported_countries() ) ) { return null; } // Get all the valid incentives. $incentives = array_filter( $this->get_incentives( $country_code ), function ( $incentive ) { return $this->validate_incentive( $incentive ); } ); // Filter by the 'connect_page' incentive type. $incentives = array_filter( $incentives, function ( $incentive ) { return 'connect_page' === $incentive['type']; } ); // Return early if there are no incentives left. if ( empty( $incentives ) ) { return null; } // Finally, return the first incentive found, if there are more than one. return reset( $incentives ); } /** * Fetches and caches eligible incentives from the WooPayments API. * * @param string $country_code The business location country code to get incentives for. * * @return array List of eligible incentives. */ private function get_incentives( string $country_code ): array { if ( isset( $this->incentives_memo ) ) { return $this->incentives_memo; } // Get the cached data. $cache = get_transient( $this->cache_transient_name ); // If the cached data is not expired, and it's a WP_Error, // it means there was an API error previously, and we should not retry just yet. if ( is_wp_error( $cache ) ) { // Initialize the in-memory cache and return it. $this->incentives_memo = []; return $this->incentives_memo; } // Gather the store context data. $store_context = [ 'country' => $country_code, // Store locale, e.g. `en_US`. 'locale' => get_locale(), // WooCommerce store active for duration in seconds. 'active_for' => WCAdminHelper::get_wcadmin_active_for_in_seconds(), 'has_orders' => $this->has_orders(), // Whether the store has at least one payment gateway enabled. 'has_payments' => ! empty( WC()->payment_gateways()->get_available_payment_gateways() ), 'has_wcpay' => $this->has_wcpay(), ]; // Fingerprint the store context through a hash of certain entries. $store_context_hash = $this->generate_context_hash( $store_context ); // Use the transient cached incentive if it exists, it is not expired, // and the store context hasn't changed since we last requested from the WooPayments API (based on context hash). if ( false !== $cache && ! empty( $cache['context_hash'] ) && is_string( $cache['context_hash'] ) && hash_equals( $store_context_hash, $cache['context_hash'] ) ) { // We have a store context hash, and it matches with the current context one. // We can use the cached incentive data. // Store the incentives in the in-memory cache and return them. $this->incentives_memo = $cache['incentives'] ?? []; return $this->incentives_memo; } // By this point, we have an expired transient or the store context has changed. // Query for incentives by calling the WooPayments API. $url = add_query_arg( $store_context, 'https://public-api.wordpress.com/wpcom/v2/wcpay/incentives', ); $response = wp_remote_get( $url, [ 'user-agent' => 'WCPay/' . WCPAY_VERSION_NUMBER . '; ' . get_bloginfo( 'url' ), ] ); // Return early if there is an error, waiting 6 hours before the next attempt. if ( is_wp_error( $response ) ) { // Store a trimmed down, lightweight error. $error = new \WP_Error( $response->get_error_code(), $response->get_error_message(), wp_remote_retrieve_response_code( $response ) ); // Store the error in the transient so we know this is due to an API error. set_transient( $this->cache_transient_name, $error, HOUR_IN_SECONDS * 6 ); // Initialize the in-memory cache and return it. $this->incentives_memo = []; return $this->incentives_memo; } $cache_for = wp_remote_retrieve_header( $response, 'cache-for' ); // Initialize the in-memory cache. $this->incentives_memo = []; if ( 200 === wp_remote_retrieve_response_code( $response ) ) { // Decode the results, falling back to an empty array. $results = json_decode( wp_remote_retrieve_body( $response ), true ) ?? []; // Store incentives in the in-memory cache. $this->incentives_memo = $results; } // Skip transient cache if `cache-for` header equals zero. if ( '0' === $cache_for ) { // If we have a transient cache that is not expired, delete it so there are no leftovers. if ( false !== $cache ) { delete_transient( $this->cache_transient_name ); } return $this->incentives_memo; } // Store incentive in the transient cache (together with the context hash) for the given number of seconds // or 1 day in seconds. Also attach a timestamp to the transient data so we know when we last fetched. set_transient( $this->cache_transient_name, [ 'incentives' => $this->incentives_memo, 'context_hash' => $store_context_hash, 'timestamp' => time(), ], ! empty( $cache_for ) ? (int) $cache_for : DAY_IN_SECONDS ); return $this->incentives_memo; } /** * Check whether the incentive data is valid. * Expects an array with at least `id`, `description`, and `tc_url` keys. * * @param mixed $incentive_data The incentive data. * * @return bool Whether the incentive data is valid. */ private function validate_incentive( $incentive_data ): bool { if ( ! is_array( $incentive_data ) || empty( $incentive_data ) || ! isset( $incentive_data['id'] ) || ! isset( $incentive_data['description'] ) || ! isset( $incentive_data['tc_url'] ) ) { return false; } return true; } /** * Check if WooPayments payment gateway was active and set up at some point, * or there are orders processed with it, at some moment. * * @return boolean Whether the store has WooPayments. */ private function has_wcpay(): bool { // First, get the stored value, if it exists. // This way we avoid costly DB queries and API calls. // Basically, we only want to know if WooPayments was in use in the past. // Since the past can't be changed, neither can this value. $had_wcpay = get_option( $this->store_had_woopayments_option_name ); if ( false !== $had_wcpay ) { return filter_var( $had_wcpay, FILTER_VALIDATE_BOOLEAN ); } // We need to determine the value. // Start with the assumption that the store didn't have WooPayments in use. $had_wcpay = false; // We consider the store to have WooPayments if there is meaningful account data in the WooPayments account cache. // This implies that WooPayments was active at some point and that it was connected. if ( $this->has_wcpay_account_data() ) { $had_wcpay = true; } // If there is at least one order processed with WooPayments, we consider the store to have WooPayments. if ( false === $had_wcpay && ! empty( wc_get_orders( [ 'payment_method' => 'woocommerce_payments', 'return' => 'ids', 'limit' => 1, 'orderby' => 'none', ] ) ) ) { $had_wcpay = true; } // Store the value for future use. update_option( $this->store_had_woopayments_option_name, $had_wcpay ? 'yes' : 'no' ); return $had_wcpay; } /** * Check if there is meaningful data in the WooPayments account cache. * * @return boolean */ private function has_wcpay_account_data(): bool { $account_data = $this->database_cache->get( Database_Cache::ACCOUNT_KEY, true ); if ( ! empty( $account_data['account_id'] ) ) { return true; } return false; } /** * Check if the store has any paid orders. * * Currently, we look at the past 90 days and only consider orders * with status `wc-completed`, `wc-processing`, or `wc-refunded`. * * @return boolean Whether the store has any paid orders. */ private function has_orders(): bool { // First, get the stored value, if it exists. // This way we avoid costly DB queries and API calls. $has_orders = get_transient( $this->store_has_orders_transient_name ); if ( false !== $has_orders ) { return filter_var( $has_orders, FILTER_VALIDATE_BOOLEAN ); } // We need to determine the value. // Start with the assumption that the store doesn't have orders in the timeframe we look at. $has_orders = false; // By default, we will check for new orders every 6 hours. $expiration = 6 * HOUR_IN_SECONDS; // Get the latest completed, processing, or refunded order. $latest_order = wc_get_orders( [ 'status' => [ 'wc-completed', 'wc-processing', 'wc-refunded' ], 'limit' => 1, 'orderby' => 'date', 'order' => 'DESC', ] ); if ( ! empty( $latest_order ) ) { $latest_order = reset( $latest_order ); // If the latest order is within the timeframe we look at, we consider the store to have orders. // Otherwise, it clearly doesn't have orders. if ( $latest_order instanceof WC_Abstract_Order && strtotime( (string) $latest_order->get_date_created() ) >= strtotime( '-90 days' ) ) { $has_orders = true; // For ultimate efficiency, we will check again after 90 days from the latest order // because in all that time we will consider the store to have orders regardless of new orders. $expiration = strtotime( (string) $latest_order->get_date_created() ) + 90 * DAY_IN_SECONDS - time(); } } // Store the value for future use. set_transient( $this->store_has_orders_transient_name, $has_orders ? 'yes' : 'no', $expiration ); return $has_orders; } /** * Generate a hash from the store context data. * * @param array $context The store context data. * * @return string The context hash. */ private function generate_context_hash( array $context ): string { // Include only certain entries in the context hash. // We need only discrete, user-interaction dependent data. // Entries like `active_for` have no place in the hash generation since they change automatically. return md5( wp_json_encode( [ 'country' => $context['country'] ?? '', 'locale' => $context['locale'] ?? '', 'has_orders' => $context['has_orders'] ?? false, 'has_payments' => $context['has_payments'] ?? false, 'has_wcpay' => $context['has_wcpay'] ?? false, ] ) ); } } includes/class-wc-payments-order-success-page.php 0000777 00000062040 15251156605 0016145 0 ustar 00 <?php /** * Class WC_Payments_Order_Success_Page * * @package WooCommerce\Payments */ use WCPay\Constants\Payment_Method; use WCPay\Duplicate_Payment_Prevention_Service; use WCPay\Core\Server\Request\Get_Intention; use WCPay\Constants\Intent_Status; use WCPay\Constants\Order_Status; /** * Class handling order success page. */ class WC_Payments_Order_Success_Page { /** * Whether to hide the blocks status description. * * @var bool */ private $should_hide_status_description = false; /** * Constructor. */ public function __construct() { add_filter( 'woocommerce_order_received_verify_known_shoppers', [ $this, 'determine_woopay_order_received_verify_known_shoppers' ], 11 ); add_action( 'woocommerce_before_thankyou', [ $this, 'register_payment_method_override' ] ); add_action( 'woocommerce_before_thankyou', [ $this, 'maybe_render_multibanco_payment_instructions' ] ); add_action( 'woocommerce_order_details_before_order_table', [ $this, 'unregister_payment_method_override' ] ); add_action( 'woocommerce_order_details_before_order_table', [ $this, 'maybe_render_multibanco_payment_instructions' ] ); add_filter( 'woocommerce_thankyou_order_received_text', [ $this, 'add_notice_previous_paid_order' ], 11 ); add_filter( 'woocommerce_thankyou_order_received_text', [ $this, 'add_notice_previous_successful_intent' ], 11 ); add_filter( 'woocommerce_thankyou_order_received_text', [ $this, 'replace_order_received_text_for_failed_orders' ], 11 ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); add_action( 'woocommerce_email_order_details', [ $this, 'add_multibanco_payment_instructions_to_order_on_hold_email' ], 10, 4 ); add_action( 'wp_footer', [ $this, 'output_footer_scripts' ] ); } /** * Register the hook to override the payment method name on the order received page. */ public function register_payment_method_override() { // Override the payment method title on the order received page. add_filter( 'woocommerce_order_get_payment_method_title', [ $this, 'show_woocommerce_payments_payment_method_name' ], 10, 2 ); } /** * Maybe render the payment instructions for Multibanco payment method. * * @param int $order_id The order ID. */ public function maybe_render_multibanco_payment_instructions( $order_id ) { if ( is_order_received_page() && current_filter() === 'woocommerce_order_details_before_order_table' ) { // Prevent rendering twice on order received page. return; } $order = wc_get_order( $order_id ); if ( ! $order || $order->get_payment_method() !== 'woocommerce_payments_' . Payment_Method::MULTIBANCO || 'on-hold' !== $order->get_status() ) { return; } $order_service = WC_Payments::get_order_service(); $multibanco_info = $order_service->get_multibanco_info_from_order( $order ); $unix_expiry = $multibanco_info['expiry']; $expiry_date = date_i18n( wc_date_format() . ' ' . wc_time_format(), $unix_expiry ); $days_remaining = max( 0, floor( ( $unix_expiry - time() ) / DAY_IN_SECONDS ) ); $formatted_order_total = $order->get_formatted_order_total(); wc_print_notice( __( 'Your order is on hold until payment is received. Please follow the payment instructions by the expiry date.', 'woocommerce-payments' ), 'notice' ); ?> <div id="wc-payment-gateway-multibanco-instructions-container"> <div class="card"> <div class="card-header"> <div class="logo-container"> <img src="<?php echo esc_url_raw( plugins_url( 'assets/images/payment-methods/multibanco-instructions.svg', WCPAY_PLUGIN_FILE ) ); ?>" alt="<?php esc_attr_e( 'Multibanco', 'woocommerce-payments' ); ?>"> </div> <div class="payment-details"> <div class="payment-header"> <?php /* translators: %s: order number */ echo esc_html( sprintf( __( 'Order #%s', 'woocommerce-payments' ), $order->get_order_number() ) ); ?> </div> <div class="payment-expiry"> <?php printf( WC_Payments_Utils::esc_interpolated_html( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped /* translators: %s: expiry date */ __( 'Expires <strong>%s</strong>', 'woocommerce-payments' ), [ 'strong' => '<strong>', ] ), esc_html( $expiry_date ) ); ?> <span class="badge"> <?php echo esc_html( sprintf( /* translators: %d: number of days */ _n( '%d day', '%d days', $days_remaining, 'woocommerce-payments' ), $days_remaining ) ); ?> </span> </div> </div> </div> <div class="payment-instructions"> <p><strong><?php esc_html_e( 'Payment instructions', 'woocommerce-payments' ); ?></strong></p> <ol> <li><?php esc_html_e( 'In your online bank account or from an ATM, choose "Payment and other services".', 'woocommerce-payments' ); ?></li> <li><?php esc_html_e( 'Click "Payments of services/shopping".', 'woocommerce-payments' ); ?></li> <li><?php esc_html_e( 'Enter the entity number, reference number, and amount.', 'woocommerce-payments' ); ?></li> </ol> </div> <div class="payment-box"> <div class="payment-box-row"> <span class="payment-box-label"><?php esc_html_e( 'Entity', 'woocommerce-payments' ); ?></span> <button type="button" class="payment-box-value copy-btn" data-copy-value="<?php echo esc_attr( $multibanco_info['entity'] ); ?>"><?php echo esc_html( $multibanco_info['entity'] ); ?><i class="copy-icon"></i></button> </div> <div class="payment-box-row"> <span class="payment-box-label"><?php esc_html_e( 'Reference', 'woocommerce-payments' ); ?></span> <button type="button" class="payment-box-value copy-btn" data-copy-value="<?php echo esc_attr( $multibanco_info['reference'] ); ?>"><?php echo esc_html( $multibanco_info['reference'] ); ?><i class="copy-icon"></i></button> </div> <div class="payment-box-row"> <span class="payment-box-label"><?php esc_html_e( 'Amount', 'woocommerce-payments' ); ?></span> <button type="button" class="payment-box-value copy-btn" data-copy-value="<?php echo esc_attr( wp_strip_all_tags( $formatted_order_total ) ); ?>"><?php echo esc_html( wp_strip_all_tags( $formatted_order_total ) ); ?><i class="copy-icon"></i></button> </div> </div> <button type="button" class="button alt print-btn"><?php esc_html_e( 'Print', 'woocommerce-payments' ); ?></button> <button type="button" class="button alt copy-link-btn copy-btn" data-copy-value="<?php echo esc_attr( $multibanco_info['url'] ); ?>"><?php esc_html_e( 'Copy link for sharing', 'woocommerce-payments' ); ?><i class="copy-icon"></i></button> </div> </div> <?php } /** * Remove the hook to override the payment method name on the order received page before the order summary. */ public function unregister_payment_method_override() { remove_filter( 'woocommerce_order_get_payment_method_title', [ $this, 'show_woocommerce_payments_payment_method_name' ], 10 ); } /** * Hooked into `woocommerce_order_get_payment_method_title` to change the payment method title on the * order received page for WooPay and BNPL orders. * * @param string $payment_method_title Original payment method title. * @param WC_Abstract_Order $abstract_order Successful received order being shown. * @return string */ public function show_woocommerce_payments_payment_method_name( $payment_method_title, $abstract_order ) { // Only change the payment method title on the order received page. if ( ! is_order_received_page() ) { return $payment_method_title; } $order_id = $abstract_order->get_id(); $order = wc_get_order( $order_id ); if ( ! $order ) { return $payment_method_title; } $payment_method_id = $order->get_payment_method(); if ( stripos( $payment_method_id, 'woocommerce_payments' ) !== 0 ) { return $payment_method_title; } // If this is a WooPay order, return the html for the WooPay payment method name. if ( $order->get_meta( 'is_woopay' ) ) { return $this->show_woopay_payment_method_name( $order ); } // Check if this is an Express Checkout payment (Google Pay, Apple Pay, etc.). $express_checkout_payment_method = $order->get_meta( '_wcpay_express_checkout_payment_method' ); if ( ! empty( $express_checkout_payment_method ) ) { return $this->show_express_checkout_payment_method_name( $order, $express_checkout_payment_method ); } $gateway = WC()->payment_gateways()->payment_gateways()[ $payment_method_id ]; if ( ! is_object( $gateway ) || ! method_exists( $gateway, 'get_payment_method' ) ) { return $payment_method_title; } $payment_method = $gateway->get_payment_method( $order ); // Handle card-based payments (Card, Link). if ( in_array( $payment_method->get_id(), [ Payment_Method::CARD ], true ) ) { return $this->show_card_payment_method_name( $order, $payment_method ); } // If this is an LPM (BNPL or local payment method) order, return the html for the payment method name. $name_output = $this->show_lpm_payment_method_name( $gateway, $payment_method ); if ( false !== $name_output ) { return $name_output; } return $payment_method_title; } /** * Returns the HTML to add the Express Checkout payment method logo and last 4 digits * of the card used to the payment method name on the order received page. * * @param WC_Order $order the order being shown. * @param string $express_checkout_payment_method the express checkout payment method (e.g., 'google_pay', 'apple_pay'). * * @return string */ public function show_express_checkout_payment_method_name( $order, $express_checkout_payment_method ) { $payment_method = WC_Payments::get_payment_method_by_id( $express_checkout_payment_method ); if ( ! $payment_method ) { return 'Payment Request'; } ob_start(); ?> <div class="wc-payment-gateway-method-logo-wrapper wc-payment-card-logo"> <img alt="<?php echo esc_attr( $payment_method->get_title() ); ?>" src="<?php echo esc_url_raw( $payment_method->get_icon() ); ?>"> <?php if ( $order->get_meta( 'last4' ) ) { echo esc_html_e( '•••', 'woocommerce-payments' ) . ' '; echo esc_html( $order->get_meta( 'last4' ) ); } ?> </div> <?php return ob_get_clean(); } /** * Returns the HTML to add the card brand logo and the last 4 digits of the card used to the * payment method name on the order received page. * * @param WC_Order $order the order being shown. * @param WCPay\Payment_Methods\UPE_Payment_Method $payment_method the payment method being shown. * * @return string */ public function show_card_payment_method_name( $order, $payment_method ) { $card_brand = $order->get_meta( '_card_brand' ); if ( ! $card_brand ) { return $payment_method->get_title(); } ob_start(); ?> <div class="wc-payment-gateway-method-logo-wrapper wc-payment-card-logo"> <img alt="<?php echo esc_attr( $payment_method->get_title() ); ?>" src="<?php echo esc_url_raw( plugins_url( "assets/images/cards/{$card_brand}.svg", WCPAY_PLUGIN_FILE ) ); ?>"> <?php if ( $order->get_meta( 'last4' ) ) { echo esc_html_e( '•••', 'woocommerce-payments' ) . ' '; echo esc_html( $order->get_meta( 'last4' ) ); } ?> </div> <?php return ob_get_clean(); } /** * Returns the HTML to add the WooPay logo and the last 4 digits of the card used to the * payment method name on the order received page. * * @param WC_Order $order the order being shown. * * @return string */ public function show_woopay_payment_method_name( $order ) { ob_start(); ?> <div class="wc-payment-gateway-method-logo-wrapper woopay"> <img alt="WooPay" src="<?php echo esc_url_raw( plugins_url( 'assets/images/payment-methods/woo-short.svg', WCPAY_PLUGIN_FILE ) ); ?>"> <?php if ( $order->get_meta( 'last4' ) ) { echo esc_html_e( 'Card ending in', 'woocommerce-payments' ) . ' '; echo esc_html( $order->get_meta( 'last4' ) ); } ?> </div> <?php return ob_get_clean(); } /** * Add the LPM logo to the payment method name on the order received page. * * @param WC_Payment_Gateway_WCPay $gateway the gateway being shown. * @param WCPay\Payment_Methods\UPE_Payment_Method $payment_method the payment method being shown. * * @return string|false */ public function show_lpm_payment_method_name( $gateway, $payment_method ) { $method_logo_url = apply_filters_deprecated( 'wc_payments_thank_you_page_bnpl_payment_method_logo_url', [ $payment_method->get_payment_method_icon_for_location( 'checkout', false, $gateway->get_account_country() ), $payment_method->get_id(), ], '8.5.0', 'wc_payments_thank_you_page_lpm_payment_method_logo_url' ); $method_logo_url = apply_filters( 'wc_payments_thank_you_page_lpm_payment_method_logo_url', $method_logo_url, $payment_method->get_id() ); // If we don't have a logo URL here for some reason, bail. if ( ! $method_logo_url ) { return false; } ob_start(); ?> <div class="wc-payment-gateway-method-logo-wrapper wc-payment-lpm-logo wc-payment-lpm-logo--<?php echo esc_attr( $payment_method->get_id() ); ?>"> <img alt="<?php echo esc_attr( $payment_method->get_title() ); ?>" title="<?php echo esc_attr( $payment_method->get_title() ); ?>" src="<?php echo esc_url_raw( $method_logo_url ); ?>"> </div> <?php return ob_get_clean(); } /** * Add the notice to the thank you page in case a recent order with the same content has already paid. * * @param string $text the default thank you text. * * @return string */ public function add_notice_previous_paid_order( $text ) { if ( isset( $_GET[ Duplicate_Payment_Prevention_Service::FLAG_PREVIOUS_ORDER_PAID ] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended $text .= $this->format_addtional_thankyou_order_received_text( __( 'We detected and prevented an attempt to pay for a duplicate order. If this was a mistake and you wish to try again, please create a new order.', 'woocommerce-payments' ) ); } return $text; } /** * Add the notice to the thank you page in case an existing intention was successful for the order. * * @param string $text the default thank you text. * * @return string */ public function add_notice_previous_successful_intent( $text ) { if ( isset( $_GET[ Duplicate_Payment_Prevention_Service::FLAG_PREVIOUS_SUCCESSFUL_INTENT ] ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended $text .= $this->format_addtional_thankyou_order_received_text( __( 'We prevented multiple payments for the same order. If this was a mistake and you wish to try again, please create a new order.', 'woocommerce-payments' ) ); } return $text; } /** * Formats the additional text to be displayed on the thank you page, with the side effect * as a workaround for an issue in Woo core 8.1.x and 8.2.x. * * @param string $additional_text The additional text to be displayed. * * @return string Formatted text. */ private function format_addtional_thankyou_order_received_text( string $additional_text ): string { /** * This condition is a workaround for Woo core 8.1.x and 8.2.x as it formatted the filtered text, * while it should format the original text only. * * It's safe to remove this conditional when WooPayments requires Woo core 8.3.x or higher. * * @see https://github.com/woocommerce/woocommerce/pull/39758 Introduce the issue since 8.1.0. * @see https://github.com/woocommerce/woocommerce/pull/40353 Fix the issue since 8.3.0. */ if ( version_compare( WC_VERSION, '8.0', '>' ) && version_compare( WC_VERSION, '8.3', '<' ) ) { echo " <script type='text/javascript'> document.querySelector('.woocommerce-thankyou-order-received')?.classList?.add('woocommerce-info'); </script> "; return ' ' . $additional_text; } return sprintf( '<div class="woocommerce-info">%s</div>', $additional_text ); } /** * Replace the order received text with a failure message when the order status is 'failed'. * * @param string $text The original thank you text. * @return string */ public function replace_order_received_text_for_failed_orders( $text ) { global $wp; $order_id = absint( $wp->query_vars['order-received'] ); $order = wc_get_order( $order_id ); if ( ! $order || ! $order->needs_payment() || 0 !== strpos( $order->get_payment_method(), WC_Payment_Gateway_WCPay::GATEWAY_ID ) ) { return $text; } $intent_id = $order->get_meta( '_intent_id', true ); $payment_method = $order->get_payment_method(); // Strip the gateway ID prefix from the payment method. $payment_method_type = str_replace( WC_Payment_Gateway_WCPay::GATEWAY_ID . '_', '', $payment_method ); $should_show_failure = false; // Check order status first to avoid unnecessary API calls. if ( $order->has_status( Order_Status::FAILED ) ) { $should_show_failure = true; } elseif ( ! empty( $intent_id ) && ! empty( $payment_method_type ) && in_array( $payment_method_type, Payment_Method::REDIRECT_PAYMENT_METHODS, true ) ) { // For redirect-based payment methods that haven't been marked as failed yet, check the intent status. // Add a small delay to allow the intent to be updated. sleep( 1 ); $intent = Get_Intention::create( $intent_id ); $intent = $intent->send(); $intent_status = $intent->get_status(); if ( Intent_Status::REQUIRES_PAYMENT_METHOD === $intent_status && $intent->get_last_payment_error() ) { $should_show_failure = true; } } if ( $should_show_failure ) { // Store the failure state to use in wp_footer. $this->should_hide_status_description = true; $checkout_url = wc_get_checkout_url(); return sprintf( /* translators: %s: checkout URL */ __( 'Unfortunately, your order has failed. Please <a href="%s">try checking out again</a>.', 'woocommerce-payments' ), esc_url( $checkout_url ) ); } return $text; } /** * Output any necessary footer scripts */ public function output_footer_scripts() { if ( ! empty( $this->should_hide_status_description ) ) { echo " <script type='text/javascript'> const element = document.querySelector('.wc-block-order-confirmation-status-description'); if (element) { element.style.display = 'none'; } </script> "; } } /** * Enqueue style to the order success page */ public function enqueue_scripts() { if ( ! is_order_received_page() && ! is_view_order_page() ) { return; } WC_Payments_Utils::enqueue_style( 'wcpay-success-css', plugins_url( 'assets/css/success.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'assets/css/success.css' ), 'all', ); WC_Payments::register_script_with_dependencies( 'WCPAY_SUCCESS_PAGE', 'dist/success', [] ); wp_set_script_translations( 'WCPAY_SUCCESS_PAGE', 'woocommerce-payments' ); wp_enqueue_script( 'WCPAY_SUCCESS_PAGE' ); } /** * Make sure we show the TYP page for orders paid with WooPay * that create new user accounts, code mainly copied from * WooCommerce WC_Shortcode_Checkout::order_received and * WC_Shortcode_Checkout::guest_should_verify_email. * * @param bool $value The current value for this filter. */ public function determine_woopay_order_received_verify_known_shoppers( $value ) { global $wp; $order_id = $wp->query_vars['order-received']; $order_key = apply_filters( 'woocommerce_thankyou_order_key', empty( $_GET['key'] ) ? '' : wc_clean( wp_unslash( $_GET['key'] ) ) ); $order = wc_get_order( $order_id ); if ( ( ! $order instanceof WC_Order ) || ! $order->get_meta( 'is_woopay' ) || ! hash_equals( $order->get_order_key(), $order_key ) ) { return $value; } $verification_grace_period = (int) apply_filters( 'woocommerce_order_email_verification_grace_period', 10 * MINUTE_IN_SECONDS, $order ); $date_created = $order->get_date_created(); // We do not need to verify the email address if we are within the grace period immediately following order creation. $is_within_grace_period = is_a( $date_created, \WC_DateTime::class, true ) && time() - $date_created->getTimestamp() <= $verification_grace_period; return ! $is_within_grace_period; } /** * Add Multibanco payment instructions to the order on-hold email. * * @param WC_Order|mixed $order The order object. * @param bool|mixed $sent_to_admin Whether the email is being sent to the admin. * @param bool|mixed $plain_text Whether the email is plain text. * @param WC_Email|string $email The email object. */ public function add_multibanco_payment_instructions_to_order_on_hold_email( $order, $sent_to_admin = false, $plain_text = false, $email = '' ): void { if ( ! $email instanceof WC_Email_Customer_On_Hold_Order || ! $order || $order->get_payment_method() !== 'woocommerce_payments_' . Payment_Method::MULTIBANCO ) { return; } $order_service = WC_Payments::get_order_service(); $multibanco_info = $order_service->get_multibanco_info_from_order( $order ); $unix_expiry = $multibanco_info['expiry']; $expiry_date = date_i18n( wc_date_format() . ' ' . wc_time_format(), $unix_expiry ); $formatted_order_total = $order->get_formatted_order_total(); if ( $plain_text ) { echo "----------------------------------------\n"; echo esc_html__( 'Multibanco Payment instructions', 'woocommerce-payments' ) . "\n\n"; printf( /* translators: %s: expiry date */ esc_html__( 'Expires %s', 'woocommerce-payments' ) . "\n\n", esc_html( $expiry_date ) ); echo '1. ' . esc_html__( 'In your online bank account or from an ATM, choose "Payment and other services".', 'woocommerce-payments' ) . "\n"; echo '2. ' . esc_html__( 'Click "Payments of services/shopping".', 'woocommerce-payments' ) . "\n"; echo '3. ' . esc_html__( 'Enter the entity number, reference number, and amount.', 'woocommerce-payments' ) . "\n\n"; echo esc_html__( 'Entity', 'woocommerce-payments' ) . ': ' . esc_html( $multibanco_info['entity'] ) . "\n"; echo esc_html__( 'Reference', 'woocommerce-payments' ) . ': ' . esc_html( $multibanco_info['reference'] ) . "\n"; echo esc_html__( 'Amount', 'woocommerce-payments' ) . ': ' . esc_html( wp_strip_all_tags( $formatted_order_total ) ) . "\n"; echo "----------------------------------------\n\n"; } else { ?> <table class="td" cellspacing="0" cellpadding="6" border="1" width="100%"> <tbody> <tr> <td class="td"> <table cellpadding="6"> <tr> <td rowspan="2" style="padding: 0 5px 0 0;"> <div style="background-color: #f6f7f7; border: 1px solid rgba( 109, 109, 109, 0.16 ); border-radius: 4px; box-sizing: border-box; padding: 10px;"> <img style="margin: 0; height: 35px; width: 35px;" src="<?php echo esc_url_raw( plugins_url( 'assets/images/payment-methods/multibanco-instructions.svg', WCPAY_PLUGIN_FILE ) ); ?>" alt="<?php esc_attr_e( 'Multibanco', 'woocommerce-payments' ); ?>"> </div> </td> <td style="font-size: 20px; padding: 0;"> <?php /* translators: %s: order number */ echo esc_html( sprintf( __( 'Order #%s', 'woocommerce-payments' ), $order->get_order_number() ) ); ?> </td> </tr> <tr> <td style="padding: 0;"> <?php printf( WC_Payments_Utils::esc_interpolated_html( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped /* translators: %s: expiry date */ __( 'Expires <strong>%s</strong>', 'woocommerce-payments' ), [ 'strong' => '<strong>', ] ), esc_html( $expiry_date ) ); ?> </td> </tr> </table> <!-- Using a paragraph to add consistent spacing between the table and the text below. --> <p></p> <p><strong><?php esc_html_e( 'Payment instructions', 'woocommerce-payments' ); ?></strong></p> <ol> <li><?php esc_html_e( 'In your online bank account or from an ATM, choose "Payment and other services".', 'woocommerce-payments' ); ?></li> <li><?php esc_html_e( 'Click "Payments of services/shopping".', 'woocommerce-payments' ); ?></li> <li><?php esc_html_e( 'Enter the entity number, reference number, and amount.', 'woocommerce-payments' ); ?></li> </ol> <table class="td" cellspacing="0" cellpadding="6" border="1" width="100%"> <tbody> <tr> <th class="td"><?php esc_html_e( 'Entity', 'woocommerce-payments' ); ?></th> <td class="td"><?php echo esc_html( $multibanco_info['entity'] ); ?></td> </tr> <tr> <th class="td"><?php esc_html_e( 'Reference', 'woocommerce-payments' ); ?></th> <td class="td"><?php echo esc_html( $multibanco_info['reference'] ); ?></td> </tr> <tr> <th class="td"><?php esc_html_e( 'Amount', 'woocommerce-payments' ); ?></th> <td class="td"><?php echo esc_html( wp_strip_all_tags( $formatted_order_total ) ); ?></td> </tr> </tbody> </table> </td> </tr> </tbody> </table> <!-- Using a paragraph to add consistent spacing between the table and the text below. --> <p></p> <?php } } } includes/class-wc-payments-payment-request-session.php 0000777 00000016775 15251156605 0017314 0 ustar 00 <?php /** * Class WC_Payments_Payment_Request_Session * * @package WooCommerce\Payments */ use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * WC_Payments_Payment_Request_Session class. */ class WC_Payments_Payment_Request_Session { /** * Name of the parameter added to the "order received" page for orders placed with the custom session handler on product pages. * * @var string Name of the parameter. */ private static $prevent_empty_cart_parameter = 'woopayments-custom-session'; /** * Used as a temporary reference to cart data, so it can be restored later. * * @var null|WC_Cart Temporary reference to cart data. */ private $cart_clone = null; /** * Init the hooks. * * @return void */ public function init() { // adding this filter with a higher priority than the session handler of the Store API. add_filter( 'woocommerce_session_handler', [ $this, 'add_payment_request_store_api_session_handler' ], 20 ); // checking to ensure we're not erasing the cart on the "order received" page. if ( $this->is_custom_session_order_received_page() ) { add_filter( 'woocommerce_persistent_cart_enabled', '__return_false' ); add_filter( 'woocommerce_cart_session_initialize', '__return_false' ); add_action( 'woocommerce_before_cart_emptied', [ $this, 'save_old_cart_data_for_restore' ] ); add_action( 'woocommerce_cart_emptied', [ $this, 'restore_old_cart_data' ] ); } } /** * Saves an instance of the current cart, so it can be restored later. * Used on the "order received" page for orders placed with the PRB. The "order received" page empties the cart, otherwise. * * @return void */ public function save_old_cart_data_for_restore() { $this->cart_clone = clone WC()->cart; } /** * Restores the cart saved previously. * * @return void */ public function restore_old_cart_data() { if ( ! $this->cart_clone ) { return; } WC()->cart->cart_contents = $this->cart_clone->cart_contents; WC()->cart->removed_cart_contents = $this->cart_clone->removed_cart_contents; WC()->cart->applied_coupons = $this->cart_clone->applied_coupons; $this->cart_clone = null; } /** * Ensuring that the return URL for the "order received" page contains a query string parameter * that can later be identified to ensure we don't clear the cart. * This function is only executed when we're using the custom session handler on Store API requests. * * @param string $return_url The URL for the "Order received" page. * * @return string */ public function store_api_order_received_return_url( $return_url ) { return add_query_arg( self::$prevent_empty_cart_parameter, '1', $return_url ); } /** * Check if the $_SERVER global has order received URL slug in its 'REQUEST_URI' value - just like `wcs_is_order_received_page`. * * Similar to WooCommerce's is_custom_session_order_received_page(), but can be used before the $wp's query vars are setup, which is essential * when preventing the cart from being emptied on the "order received" page, if the order has been placed with WooPayments GooglePay/ApplePay on the product page. * * @return bool **/ private function is_custom_session_order_received_page() { // ignoring because we're not storing the value anywhere, just checking its existence. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash return ( false !== strpos( $_SERVER['REQUEST_URI'], 'order-received' ) ) && ( false !== strpos( $_SERVER['REQUEST_URI'], self::$prevent_empty_cart_parameter ) ); } /** * Generates a session token for the response headers. * * @return string */ protected function get_session_token() { return JsonWebToken::create( [ 'session_id' => WC()->session->session_id, 'exp' => time() + intval( apply_filters( 'wc_session_expiration', DAY_IN_SECONDS * 2 ) ), 'iss' => 'woopayments/product-page', ], '@' . wp_salt() ); } /** * Adding the session key to the Store API response, to ensure the session can be retrieved later. * * @param mixed $response Response to replace the requested version with. * * @return mixed */ public function store_api_headers( $response ) { $response->header( 'X-WooPayments-Tokenized-Cart-Session', $this->get_session_token() ); return $response; } /** * Adding the session key to the Store API response, to ensure the session can be retrieved later. * * @param mixed $response Response to replace the requested version with. * @param \WP_REST_Server $server Server instance. * @param \WP_REST_Request $request Request used to generate the response. * * @return mixed */ public function maybe_clear_cart_data( $response, $server, $request ) { if ( $request->get_header( 'X-WooPayments-Tokenized-Cart-Is-Ephemeral-Cart' ) === '1' ) { // the customer id value doesn't matter. // in this case, we'll be using the `WC_Payments_Payment_Request_Session_Handler` session handler, // which will use the correct customer ID to delete. // I am specifically calling `delete_session` instead of `forget_session` or `destroy_session`, // because those methods might delete the customer's cookies (which we want to keep). WC()->session->delete_session( 0 ); WC()->cart->empty_cart(); } return $response; } /** * This filter is used to add a custom session handler before processing Store API request callbacks. * This is only necessary because the Store API SessionHandler currently doesn't provide an `init_session_cookie` method. * * @param string $default_session_handler The default session handler class name. * * @return string The session handler class name. */ public function add_payment_request_store_api_session_handler( $default_session_handler ) { $nonce = wc_clean( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION_NONCE'] ?? null ) ); if ( ! wp_verify_nonce( $nonce, 'woopayments_tokenized_cart_session_nonce' ) ) { return $default_session_handler; } if ( ! \WC_Payments_Utils::is_store_api_request() ) { return $default_session_handler; } if ( ! class_exists( JsonWebToken::class ) ) { return $default_session_handler; } // checking if the token is valid, if it's provided. // there can also be a case where the token is not provided, but we should still use the custom session handler. $cart_token = wc_clean( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_SESSION'] ?? null ) ); if ( $cart_token && ! JsonWebToken::validate( $cart_token, '@' . wp_salt() ) ) { return $default_session_handler; } // ensures cart contents aren't merged across different sessions for the same customer. add_filter( 'woocommerce_persistent_cart_enabled', '__return_false' ); // when an order is placed via the Store API on product pages, we need to slightly modify the "order received" URL. add_filter( 'woocommerce_get_return_url', [ $this, 'store_api_order_received_return_url' ] ); // ensuring that the `X-WooPayments-Tokenized-Cart-Session` response header is added to the response. add_filter( 'rest_post_dispatch', [ $this, 'store_api_headers' ] ); // clearing the cart contents if the request is made just to fetch product attributes and prices. add_filter( 'rest_post_dispatch', [ $this, 'maybe_clear_cart_data' ], 10, 3 ); require_once WCPAY_ABSPATH . '/includes/class-wc-payments-payment-request-session-handler.php'; return WC_Payments_Payment_Request_Session_Handler::class; } } includes/wc-payment-api/class-wc-payments-http-interface.php 0000777 00000005751 15251156605 0020230 0 ustar 00 <?php /** * WC_Payments_Http_Interface class. * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; use WCPay\Exceptions\API_Exception; /** * A wrapper around Jetpack HTTP request library. Necessary to increase * the testability of WC_Payments_API_Client, and allow dependency * injection. */ interface WC_Payments_Http_Interface { /** * Sends a remote request through Jetpack. * * @param array $args - The arguments to passed to Jetpack. * @param string $body - The body passed on to the HTTP request. * @param bool $is_site_specific - If true, the site ID will be included in the request url. Defaults to true. * @param bool $use_user_token - If true, the request will be signed with the user token rather than blog token. Defaults to false. * * @return array HTTP response on success. * @throws API_Exception - If not connected or request failed. */ public function remote_request( $args, $body = null, $is_site_specific = true, $use_user_token = false ); /** * Checks if Jetpack is connected. * * Checks if connection is authenticated in the same way as Jetpack_Client or Jetpack Connection Client does. * * @return bool true if Jetpack connection has access token. */ public function is_connected(); /** * Checks if the site has an admin who is also a connection owner. * * @return bool True if Jetpack connection has an owner. */ public function has_connection_owner(); /** * Checks if the current user is connected to WordPress.com. * * @return bool true if the current user is connected. */ public function is_user_connected(); /** * Get the wpcom user data of the current connected user. * * @return bool|array An array with the WPCOM user data on success, false otherwise. */ public function get_connected_user_data(); /** * Gets the current WP.com blog ID. * * @return integer Current WPCOM blog ID. */ public function get_blog_id(); /** * Starts the Jetpack connection process. Note that running this function will immediately redirect * to the Jetpack flow, so any PHP code after it will never be executed. * * @param string $redirect - URL to redirect to after the connection process is over. * * @throws API_Exception - Exception thrown on failure. */ public function start_connection( $redirect ); /** * Queries the WordPress.com REST API with a user token. * * @param string $path REST API path. * @param string $version REST API version. Default is `2`. * @param array $args Arguments to {@see WP_Http}. Default is `array()`. * @param string|array $body Body passed to {@see WP_Http}. Default is `null`. * @param string $base_api_path REST API root. Default is `wpcom`. * * @return array|WP_Error $response Response data, else {@see WP_Error} on failure. */ public function wpcom_json_api_request_as_user( $path, $version = '2', $args = [], $body = null, $base_api_path = 'wpcom' ); } includes/wc-payment-api/class-wc-payments-api-client.php 0000777 00000264354 15251156605 0017346 0 ustar 00 <?php /** * WC_Payments_API_Client class * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; use WCPay\Constants\Intent_Status; use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\API_Merchant_Exception; use WCPay\Exceptions\Amount_Too_Small_Exception; use WCPay\Exceptions\Amount_Too_Large_Exception; use WCPay\Exceptions\Connection_Exception; use WCPay\Fraud_Prevention\Fraud_Prevention_Service; use WCPay\Fraud_Prevention\Buyer_Fingerprinting_Service; use WCPay\Logger; use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore; use WCPay\Constants\Currency_Code; use WCPay\Database_Cache; use WCPay\Core\Server\Request; use WCPay\Core\Server\Request\List_Fraud_Outcome_Transactions; use WCPay\Exceptions\Cannot_Combine_Currencies_Exception; use WCPay\MultiCurrency\Interfaces\MultiCurrencyApiClientInterface; /** * Communicates with WooCommerce Payments API. */ class WC_Payments_API_Client implements MultiCurrencyApiClientInterface { const ENDPOINT_BASE = 'https://public-api.wordpress.com/wpcom/v2'; const ENDPOINT_SITE_FRAGMENT = 'sites/%s'; const ENDPOINT_REST_BASE = 'wcpay'; const V2_ENDPOINT_REST_BASE = 'transact'; const POST = 'POST'; const GET = 'GET'; const DELETE = 'DELETE'; const API_TIMEOUT_SECONDS = 70; const API_RETRIES_LIMIT = 3; const API_RETRIES_BACKOFF_MSEC = 250; const ACCOUNTS_API = 'accounts'; const CAPABILITIES_API = 'accounts/capabilities'; const WOOPAY_ACCOUNTS_API = 'accounts/platform_checkout'; const WOOPAY_COMPATIBILITY_API = 'woopay/compatibility'; const DOMAIN_REGISTRATION_API = 'payment_method_domains'; const CHARGES_API = 'charges'; const CONN_TOKENS_API = 'terminal/connection_tokens'; const TERMINAL_LOCATIONS_API = 'terminal/locations'; const CUSTOMERS_API = 'customers'; const CURRENCY_API = 'currency'; const INTENTIONS_API = 'intentions'; const REFUNDS_API = 'refunds'; const DEPOSITS_API = 'deposits'; const TRANSACTIONS_API = 'transactions'; const DISPUTES_API = 'disputes'; const FILES_API = 'files'; const ONBOARDING_API = 'onboarding'; const TIMELINE_API = 'timeline'; const PAYMENT_METHODS_API = 'payment_methods'; const SETUP_INTENTS_API = 'setup_intents'; const TRACKING_API = 'tracking'; const PRODUCTS_API = 'products'; const PRICES_API = 'products/prices'; const PAYMENT_PROCESS_CONFIG_API = 'payment_process_config'; const INVOICES_API = 'invoices'; const SUBSCRIPTIONS_API = 'subscriptions'; const SUBSCRIPTION_ITEMS_API = 'subscriptions/items'; const READERS_CHARGE_SUMMARY = 'reader-charges/summary'; const TERMINAL_READERS_API = 'terminal/readers'; const MINIMUM_RECURRING_AMOUNT_API = 'subscriptions/minimum_amount'; const CAPITAL_API = 'capital'; const WEBHOOK_FETCH_API = 'webhook/failed_events'; const DOCUMENTS_API = 'documents'; const VAT_API = 'vat'; const LINKS_API = 'links'; const AUTHORIZATIONS_API = 'authorizations'; const FRAUD_SERVICES_API = 'accounts/fraud_services'; const FRAUD_OUTCOMES_API = 'fraud_outcomes'; const FRAUD_RULESET_API = 'fraud_ruleset'; const COMPATIBILITY_API = 'compatibility'; const RECOMMENDED_PAYMENT_METHODS = 'payment_methods/recommended'; const ADDRESS_AUTOCOMPLETE_TOKEN = 'address-autocomplete-token'; const STORE_SETUP_API = 'accounts/store_setup'; const PROMOTIONS_API = 'payment_method_promotions'; /** * Common keys in API requests/responses that we might want to redact. */ const API_KEYS_TO_REDACT = [ 'client_secret', 'email', 'name', 'first_name', 'last_name', 'phone', 'company', 'address_1', 'address_2', 'line1', 'line2', 'postal_code', 'postcode', 'state', 'city', 'country', 'company', 'customer_name', 'customer_email', ]; const EVENT_AUTHORIZED = 'authorized'; const EVENT_AUTHORIZATION_VOIDED = 'authorization_voided'; const EVENT_AUTHORIZATION_EXPIRED = 'authorization_expired'; const EVENT_CAPTURED = 'captured'; const EVENT_PARTIAL_REFUND = 'partial_refund'; const EVENT_FULL_REFUND = 'full_refund'; const EVENT_REFUND_FAILURE = 'refund_failed'; const EVENT_FAILED = 'failed'; // const EVENT_BLOCKED = 'blocked'; // no event for this. const EVENT_DISPUTE_NEEDS_RESPONSE = 'dispute_needs_response'; const EVENT_DISPUTE_IN_REVIEW = 'dispute_in_review'; const EVENT_DISPUTE_WON = 'dispute_won'; const EVENT_DISPUTE_LOST = 'dispute_lost'; // const EVENT_DISPUTE_ACCEPTED = 'dispute_accepted'; // set as 'lost' in the API. const EVENT_DISPUTE_WARNING_CLOSED = 'dispute_warning_closed'; const EVENT_DISPUTE_CHARGE_REFUNDED = 'dispute_charge_refunded'; const EVENT_FINANCING_PAYDOWN = 'financing_paydown'; const ARN_UNAVAILABLE_STATUS = 'unavailable'; const EVENT_FRAUD_OUTCOME_REVIEW = 'fraud_outcome_review'; const EVENT_FRAUD_OUTCOME_BLOCK = 'fraud_outcome_block'; /** * An array used to determine the order of events in case they share the same timestamp * * @var array */ private static $events_order = [ self::EVENT_AUTHORIZED, self::EVENT_AUTHORIZATION_VOIDED, self::EVENT_AUTHORIZATION_EXPIRED, self::EVENT_FRAUD_OUTCOME_REVIEW, self::EVENT_FRAUD_OUTCOME_BLOCK, self::EVENT_CAPTURED, self::EVENT_PARTIAL_REFUND, self::EVENT_FULL_REFUND, self::EVENT_REFUND_FAILURE, self::EVENT_FAILED, // self::EVENT_BLOCKED, uncomment when needed. self::EVENT_DISPUTE_NEEDS_RESPONSE, self::EVENT_DISPUTE_IN_REVIEW, self::EVENT_DISPUTE_WON, self::EVENT_DISPUTE_LOST, // self::EVENT_DISPUTE_ACCEPTED, uncommented when needed. self::EVENT_FINANCING_PAYDOWN, ]; /** * User agent string to report in requests. * * @var string */ private $user_agent; /** * An HTTP client implementation used to send HTTP requests. * * @var WC_Payments_Http */ private $http_client; /** * DB access wrapper. * * @var WC_Payments_DB */ private $wcpay_db; /** * WC_Payments_API_Client constructor. * * @param string $user_agent - User agent string to report in requests. * @param WC_Payments_Http $http_client - Used to send HTTP requests. * @param WC_Payments_DB $wcpay_db - DB access wrapper. */ public function __construct( $user_agent, $http_client, $wcpay_db ) { $this->user_agent = $user_agent; $this->http_client = $http_client; $this->wcpay_db = $wcpay_db; } /** * Whether the site can communicate with the WCPay server (i.e. Jetpack connection has been established). * * @return bool */ public function is_server_connected(): bool { return $this->http_client->is_connected(); } /** * Checks if the site has an admin who is also a connection owner. * * @return bool True if Jetpack connection has an owner. */ public function has_server_connection_owner() { return $this->http_client->has_connection_owner(); } /** * Gets the current WP.com blog ID, if the Jetpack connection has been set up. * * @return integer|NULL Current WPCOM blog ID, or NULL if not connected yet. */ public function get_blog_id() { return $this->is_server_connected() ? $this->http_client->get_blog_id() : null; } /** * Starts the Jetpack connection process. Note that running this function will immediately redirect * to the Jetpack flow, so any PHP code after it will never be executed. * * @param string $redirect - URL to redirect to after the connection process is over. * * @throws API_Exception - Exception thrown on failure. */ public function start_server_connection( $redirect ) { $this->http_client->start_connection( $redirect ); } /** * Fetch a single intent with provided id. * * @param string $intent_id intent id. * * @return WC_Payments_API_Payment_Intention intention object. * @throws API_Exception - Exception thrown in case route validation fails. */ public function get_intent( $intent_id ) { if ( ! preg_match( '/^\w+$/', $intent_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } $intent = $this->request( [], self::INTENTIONS_API . '/' . $intent_id, self::GET ); return $this->deserialize_payment_intention_object_from_array( $intent ); } /** * Get summary of deposits. * * @param array $filters The filters to be used in the query. * * @return array * @throws API_Exception - Exception thrown on request failure. */ public function get_deposits_summary( array $filters = [] ) { return $this->request( $filters, self::DEPOSITS_API . '/summary', self::GET ); } /** * Trigger a manual deposit. * * @param string $type Type of deposit. Only "instant" is supported for now. * @param string $currency The deposit currency. * @return array The new deposit object. * @throws API_Exception - Exception thrown on request failure. */ public function manual_deposit( $type, $currency ) { return $this->request( [ 'type' => $type, 'currency' => $currency, ], self::DEPOSITS_API, self::POST ); } /** * Return summary for transactions. * * @param array $filters The filters to be used in the query. * @param string $deposit_id The deposit to filter on. * * @return array The transactions summary. * @throws API_Exception Exception thrown on request failure. */ public function get_transactions_summary( $filters = [], $deposit_id = null ) { // Map Order # terms to the actual charge id to be used in the server. if ( ! empty( $filters['search'] ) ) { $filters['search'] = WC_Payments_Utils::map_search_orders_to_charge_ids( $filters['search'] ); } $query = array_merge( $filters, [ 'deposit_id' => $deposit_id, ] ); return $this->request( $query, self::TRANSACTIONS_API . '/summary', self::GET ); } /** * Retrieves transaction list for a given fraud outcome status. * * @param List_Fraud_Outcome_Transactions $request Fraud outcome transactions request. * * @return array */ public function list_fraud_outcome_transactions( $request ) { // TODO: Refactor this. $request->assign_hook( 'wcpay_list_fraud_outcome_transactions_request' ); $fraud_outcomes = $request->send(); $page = $request->get_param( 'page' ); $page_size = $request->get_param( 'pagesize' ); // Handles the pagination. $fraud_outcomes = array_slice( $fraud_outcomes, ( max( $page, 1 ) - 1 ) * $page_size, $page_size ); return [ 'data' => $fraud_outcomes, ]; } /** * Retrieves transactions summary for a given fraud outcome status. * * @param List_Fraud_Outcome_Transactions $request Fraud outcome transactions request. * * @return array */ public function list_fraud_outcome_transactions_summary( $request ) { // TODO: Refactor this. $request->assign_hook( 'wcpay_list_fraud_outcome_transactions_summary_request' ); $fraud_outcomes = $request->send(); $total = 0; $currencies = []; foreach ( $fraud_outcomes as $outcome ) { $total += $outcome['amount']; $currencies[] = strtolower( $outcome['currency'] ); } return [ 'count' => is_countable( $fraud_outcomes ) ? count( $fraud_outcomes ) : 0, 'total' => (int) $total, 'currencies' => array_unique( $currencies ), ]; } /** * Fetch transactions search options for provided query. * * @param List_Fraud_Outcome_Transactions $request Fraud outcome transactions request. * * @return array|WP_Error Search results. */ public function get_fraud_outcome_transactions_search_autocomplete( $request ) { // TODO: Refactor this. $request->assign_hook( 'wcpay_get_fraud_outcome_transactions_search_autocomplete_request' ); $fraud_outcomes = $request->send(); $search_term = $request->get_param( 'search_term' ); $order = wc_get_order( $search_term ); $results = array_filter( $fraud_outcomes, function ( $outcome ) use ( $search_term ) { return preg_match( "/{$search_term}/i", $outcome['customer_name'] ); } ); $results = array_map( function ( $result ) { return [ 'key' => 'customer-' . $result['order_id'], 'label' => $result['customer_name'], ]; }, $fraud_outcomes ); if ( $order ) { if ( function_exists( 'wcs_is_subscription' ) && wcs_is_subscription( $order ) ) { $prefix = __( 'Subscription #', 'woocommerce-payments' ); } else { $prefix = __( 'Order #', 'woocommerce-payments' ); } array_unshift( $results, [ 'key' => 'order-' . $order->get_id(), 'label' => $prefix . $search_term, ] ); } return $results; } /** * Retrieves transactions summary for a given fraud outcome status. * * @param List_Fraud_Outcome_Transactions $request Fraud outcome transactions request. * * @return array */ public function get_fraud_outcome_transactions_export( $request ) { // TODO: Refactor this. $request->assign_hook( 'wcpay_get_fraud_outcome_transactions_export_request' ); $fraud_outcomes = $request->send(); return [ 'data' => $fraud_outcomes, ]; } /** * Initiates transactions export via API. * * @param array $filters The filters to be used in the query. * @param string $user_email The email to search for. * @param string $deposit_id The deposit to filter on. * @param string $locale Site locale. * * @return array Export summary * * @throws API_Exception - Exception thrown on request failure. */ public function get_transactions_export( $filters = [], $user_email = '', $deposit_id = null, $locale = null ) { // Map Order # terms to the actual charge id to be used in the server. if ( ! empty( $filters['search'] ) ) { $filters['search'] = WC_Payments_Utils::map_search_orders_to_charge_ids( $filters['search'] ); } if ( ! empty( $user_email ) ) { $filters['user_email'] = $user_email; } if ( ! empty( $deposit_id ) ) { $filters['deposit_id'] = $deposit_id; } if ( ! empty( $locale ) ) { $filters['locale'] = $locale; } return $this->request( $filters, self::TRANSACTIONS_API . '/download', self::POST ); } /** * Get the transactions export URL for a given export ID, if available. * * @param string $export_id The export ID. * * @return array The export URL response. * @throws API_Exception - Exception thrown on request failure. */ public function get_transactions_export_url( string $export_id ): array { return $this->request( [], self::TRANSACTIONS_API . "/download/{$export_id}", self::GET ); } /** * Get the disputes export URL for a given export ID, if available. * * @param string $export_id The export ID. * * @return array The export URL response. * @throws API_Exception - Exception thrown on request failure. */ public function get_disputes_export_url( string $export_id ): array { return $this->request( [], self::DISPUTES_API . "/download/{$export_id}", self::GET ); } /** * Get the payouts export URL for a given export ID, if available. * * @param string $export_id The export ID. * * @return array The export URL response. * @throws API_Exception - Exception thrown on request failure. */ public function get_payouts_export_url( string $export_id ): array { return $this->request( [], self::DEPOSITS_API . "/download/{$export_id}", self::GET ); } /** * Fetch account recommended payment methods data for a given country. * * @param string $country_code The account's business location country code. Provide a 2-letter ISO country code. * @param string $locale Optional. The locale to instruct the platform to use for i18n. * * @return array The recommended payment methods data. * @throws API_Exception Exception thrown on request failure. */ public function get_recommended_payment_methods( string $country_code, string $locale = '' ): array { // We can't use the request method here because this route doesn't require a connected store // and we request this data pre-onboarding. // By this point, we have an expired transient or the store context has changed. // Query for incentives by calling the WooPayments API. $url = add_query_arg( [ 'country_code' => $country_code, 'locale' => $locale, ], self::ENDPOINT_BASE . '/' . self::ENDPOINT_REST_BASE . '/' . self::RECOMMENDED_PAYMENT_METHODS, ); $response = wp_remote_get( $url, [ 'headers' => apply_filters( 'wcpay_api_request_headers', [ 'Content-type' => 'application/json; charset=utf-8', ] ), 'user-agent' => $this->user_agent, 'timeout' => self::API_TIMEOUT_SECONDS, 'sslverify' => false, ] ); if ( is_wp_error( $response ) ) { Logger::error( 'HTTP_REQUEST_ERROR ' . var_export( $response, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export $message = sprintf( // translators: %1: original error message. __( 'Http request failed. Reason: %1$s', 'woocommerce-payments' ), $response->get_error_message() ); throw new API_Exception( $message, 'wcpay_http_request_failed', 500 ); } $results = []; if ( 200 === wp_remote_retrieve_response_code( $response ) ) { // Decode the results, falling back to an empty array. $results = $this->extract_response_body( $response ); if ( ! is_array( $results ) ) { $results = []; } } return $results; } /** * Fetch a single transaction with provided id. * * @param string $transaction_id id of requested transaction. * @return array transaction object. * @throws API_Exception - Exception thrown in case route validation fails. */ public function get_transaction( $transaction_id ) { if ( ! preg_match( '/^\w+$/', $transaction_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } $transaction = $this->request( [], self::TRANSACTIONS_API . '/' . $transaction_id, self::GET ); if ( is_wp_error( $transaction ) ) { return $transaction; } return $this->add_order_info_to_charge_object( $transaction['charge_id'], $transaction ); } /** * Fetch transactions search options for provided query. * * @param string $search_term Query to be used to get search options - can be an order ID, or part of a name or email. * @return array|WP_Error Search results. */ public function get_transactions_search_autocomplete( $search_term ) { $order = wc_get_order( $search_term ); $search_results = $this->request( [ 'search_term' => $search_term ], self::TRANSACTIONS_API . '/search', self::GET ); $results = array_map( function ( $result ) { return [ 'label' => $result['customer_name'] . ' (' . $result['customer_email'] . ')', ]; }, $search_results ); if ( $order ) { if ( function_exists( 'wcs_is_subscription' ) && wcs_is_subscription( $order ) ) { $prefix = __( 'Subscription #', 'woocommerce-payments' ); } else { $prefix = __( 'Order #', 'woocommerce-payments' ); } array_unshift( $results, [ 'label' => $prefix . $search_term ] ); } return $results; } /** * Get summary of disputes. * * @param array $filters The filters to be used in the query. * * @return array * @throws API_Exception - Exception thrown on request failure. */ public function get_disputes_summary( array $filters = [] ): array { return $this->request( [ $filters ], self::DISPUTES_API . '/summary', self::GET ); } /** * Fetch disputes by provided query. * * @param array $filters Query to be used to get disputes. * @return array Disputes. * @throws API_Exception - Exception thrown on request failure. */ public function get_disputes( array $filters = [] ) { return $this->request( $filters, self::DISPUTES_API, self::GET ); } /** * Fetch a single dispute with provided id. * * @param string $dispute_id id of requested dispute. * @return array dispute object. * @throws API_Exception - Exception thrown in case route validation fails. */ public function get_dispute( $dispute_id ) { if ( ! preg_match( '/^\w+$/', $dispute_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } $dispute = $this->request( [], self::DISPUTES_API . '/' . $dispute_id, self::GET ); if ( is_wp_error( $dispute ) ) { return $dispute; } $charge_id = is_array( $dispute['charge'] ) ? $dispute['charge']['id'] : $dispute['charge']; return $this->add_order_info_to_charge_object( $charge_id, $dispute ); } /** * Update dispute with provided id. * * @param string $dispute_id id of dispute to update. * @param array $evidence evidence to upload. * @param bool $submit whether to submit (rather than stage) evidence. * @param array $metadata metadata associated with this dispute. * * @return array dispute object. * @throws API_Exception - Exception thrown in case route validation fails. */ public function update_dispute( $dispute_id, $evidence, $submit, $metadata ) { if ( ! preg_match( '/^\w+$/', $dispute_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } // Fetch the dispute to check if it's a Visa compliance (noncompliant) dispute. $dispute_details = $this->get_dispute( $dispute_id ); if ( is_wp_error( $dispute_details ) ) { return $dispute_details; } $request = [ 'evidence' => $evidence, 'submit' => $submit, 'metadata' => $metadata, ]; // Add Visa compliance flag for noncompliant disputes. if ( isset( $dispute_details['reason'] ) && 'noncompliant' === $dispute_details['reason'] ) { $request['evidence']['enhanced_evidence'] = array_merge( $request['evidence']['enhanced_evidence'] ?? [], [ 'visa_compliance' => [ 'fee_acknowledged' => 'true', ], ] ); } $dispute = $this->request( $request, self::DISPUTES_API . '/' . $dispute_id, self::POST ); // Invalidate the dispute caches. \WC_Payments::get_database_cache()->delete_dispute_caches(); if ( is_wp_error( $dispute ) ) { return $dispute; } $charge_id = is_array( $dispute['charge'] ) ? $dispute['charge']['id'] : $dispute['charge']; return $this->add_order_info_to_charge_object( $charge_id, $dispute ); } /** * Close dispute with provided id. * * @param string $dispute_id id of dispute to close. * @return array dispute object. * * @throws API_Exception - Exception thrown in case route validation fails. */ public function close_dispute( $dispute_id ) { if ( ! preg_match( '/^\w+$/', $dispute_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } $dispute = $this->request( [], self::DISPUTES_API . '/' . $dispute_id . '/close', self::POST ); // Invalidate the dispute caches. \WC_Payments::get_database_cache()->delete_dispute_caches(); if ( is_wp_error( $dispute ) ) { return $dispute; } $charge_id = is_array( $dispute['charge'] ) ? $dispute['charge']['id'] : $dispute['charge']; return $this->add_order_info_to_charge_object( $charge_id, $dispute ); } /** * Initiates disputes export via API. * * @param array $filters The filters to be used in the query. * @param string $user_email The email to search for. * @param string $locale Site locale. * * @return array Export summary * * @throws API_Exception - Exception thrown on request failure. */ public function get_disputes_export( $filters = [], $user_email = '', $locale = null ) { if ( ! empty( $user_email ) ) { $filters['user_email'] = $user_email; } if ( ! empty( $locale ) ) { $filters['locale'] = $locale; } return $this->request( $filters, self::DISPUTES_API . '/download', self::POST ); } /** * Initiates deposits export via API. * * @param array $filters The filters to be used in the query. * @param string $user_email The email to send export to. * @param string $locale Site locale. * * @return array Export summary * * @throws API_Exception - Exception thrown on request failure. */ public function get_deposits_export( $filters = [], $user_email = '', $locale = null ) { if ( ! empty( $user_email ) ) { $filters['user_email'] = $user_email; } if ( ! empty( $locale ) ) { $filters['locale'] = $locale; } return $this->request( $filters, self::DEPOSITS_API . '/download', self::POST ); } /** * Upload file and return file object. * * @param WP_REST_Request $request request object received. * * @return array file object. * @throws API_Exception - If request throws. */ public function upload_file( $request ) { $purpose = $request->get_param( 'purpose' ); $file_params = $request->get_file_params(); $file_name = $file_params['file']['name']; $file_type = $file_params['file']['type']; $as_account = (bool) $request->get_param( 'as_account' ); // Sometimes $file_params is empty array for large files (8+ MB). $file_error = empty( $file_params ) || $file_params['file']['error']; if ( $file_error ) { // TODO - Add better error message by specifiying which limit is reached (host or Stripe). throw new API_Exception( __( 'Max file size exceeded.', 'woocommerce-payments' ), 'wcpay_evidence_file_max_size', 400 ); } $body = [ // We disable php linting here because otherwise it will show a warning on improper // use of `file_get_contents()` and say you should "use `wp_remote_get()` for // remote URLs instead", which is unrelated to our use here. // phpcs:disable 'file' => base64_encode( file_get_contents( $file_params['file']['tmp_name'] ) ), // phpcs:enable 'file_name' => $file_name, 'file_type' => $file_type, 'purpose' => $purpose, 'as_account' => $as_account, ]; try { return $this->request( $body, self::FILES_API, self::POST ); } catch ( API_Exception $e ) { throw new API_Exception( $e->getMessage(), 'wcpay_evidence_file_upload_error', $e->get_http_code() ); } } /** * Retrieve a file content via API. * * @param string $file_id - API file id. * @param bool $as_account - add the current account to header request. * * @return array * @throws API_Exception */ public function get_file_contents( string $file_id, bool $as_account = true ): array { try { if ( ! preg_match( '/^\w+$/', $file_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [ 'as_account' => $as_account ], self::FILES_API . '/' . $file_id . '/contents', self::GET ); } catch ( API_Exception $e ) { Logger::error( 'Error retrieving file contents for ' . $file_id . '. ' . $e->getMessage() ); return []; } } /** * Retrieve a file details via API. * * @param string $file_id - API file id. * @param bool $as_account - add the current account to header request. * * @return array * @throws API_Exception */ public function get_file( string $file_id, bool $as_account = true ): array { if ( ! preg_match( '/^\w+$/', $file_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [ 'as_account' => $as_account ], self::FILES_API . '/' . $file_id, self::GET ); } /** * Create a connection token. * * @param WP_REST_Request $request request object received. * * @return array * @throws API_Exception - If request throws. */ public function create_token( $request ) { return $this->request( [], self::CONN_TOKENS_API, self::POST ); } /** * Get timeline of events for an intention * * @param string $id The payment intention ID or order ID. * * @return array * * @throws Exception - Exception thrown on request failure. * @throws API_Exception - Exception thrown in case route validation fails. */ public function get_timeline( $id ) { if ( ! preg_match( '/^\w+$/', $id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } $timeline = $this->request( [], self::TIMELINE_API . '/' . $id, self::GET ); $has_fraud_outcome_event = false; if ( ! empty( $timeline ) && ! empty( $timeline['data'] ) && is_array( $timeline['data'] ) ) { foreach ( $timeline['data'] as $event ) { if ( in_array( $event['type'], [ self::EVENT_FRAUD_OUTCOME_REVIEW, self::EVENT_FRAUD_OUTCOME_BLOCK ], true ) ) { $has_fraud_outcome_event = true; break; } } } if ( $has_fraud_outcome_event ) { $order_id = $id; if ( ! is_numeric( $order_id ) ) { $intent = $this->get_intent( $id ); $order_id = $intent->get_metadata()['order_id']; } $order = wc_get_order( $order_id ); if ( false === $order ) { return $timeline; } $manual_entry_meta = $order->get_meta( '_wcpay_fraud_outcome_manual_entry', true ); if ( ! empty( $manual_entry_meta ) ) { $timeline['data'][] = $manual_entry_meta; // Sort by date desc, then by type desc as specified in events_order. usort( $timeline['data'], function ( $a, $b ) { $result = $b['datetime'] <=> $a['datetime']; if ( 0 !== $result ) { return $result; } return array_search( $b['type'], self::$events_order, true ) <=> array_search( $a['type'], self::$events_order, true ); } ); } } return $timeline; } /** * Get currency rates from the server. * * @param string $currency_from - The currency to convert from. * @param ?array $currencies_to - An array of the currencies we want to convert into. If left empty, will get all supported currencies. * * @return array * * @throws API_Exception - Error contacting the API. */ public function get_currency_rates( string $currency_from, $currencies_to = null ): array { if ( empty( $currency_from ) ) { throw new API_Exception( __( 'Currency From parameter is required', 'woocommerce-payments' ), 'wcpay_mandatory_currency_from_missing', 400 ); } $query_body = [ 'currency_from' => $currency_from ]; if ( null !== $currencies_to ) { $query_body['currencies_to'] = $currencies_to; } return $this->request( $query_body, self::CURRENCY_API . '/rates', self::GET, true, false, false, true ); } /** * Get current woopay eligibility * * @return array An array describing woopay eligibility. * * @throws API_Exception - Error contacting the API. */ public function get_woopay_eligibility() { return $this->request( [ 'test_mode' => WC_Payments::mode()->is_test_mode_onboarding(), // only send a test mode request if in test mode onboarding. ], self::WOOPAY_ACCOUNTS_API, self::GET ); } /** * Update woopay data * * @param array $data Data to update. * * @return array An array describing request result. * * @throws API_Exception - Error contacting the API. */ public function update_woopay( $data ) { return $this->request( array_merge( [ 'test_mode' => WC_Payments::mode()->is_test_mode_onboarding() ], $data ), self::WOOPAY_ACCOUNTS_API, self::POST ); } /** * Request capability activation from the server * * @param string $capability_id Capability ID. * @param bool $requested State. * * @return array Request result. */ public function request_capability( string $capability_id, bool $requested ) { return $this->request( [ 'capability_id' => $capability_id, 'requested' => $requested, ], self::CAPABILITIES_API, self::POST, true, true ); } /** * Get data needed to initialize the onboarding flow * * @param bool $live_account Whether to get the onboarding data for a live mode or test mode account. * @param string $return_url URL to redirect to at the end of the flow. * @param array $site_data Data to track ToS agreement. * @param array $user_data Data about the user doing the onboarding (location and device). * @param array $account_data Data to prefill the onboarding. * @param array $actioned_notes Actioned WCPay note names to be sent to the onboarding flow. * @param bool $collect_payout_requirements Whether we need to redirect user to Stripe KYC to complete their payouts data. * @param ?string $referral_code Referral code to be used for onboarding. * * @return array An array containing the url and state fields. * @throws API_Exception Exception thrown on request failure. */ public function get_onboarding_data( bool $live_account, string $return_url, array $site_data = [], array $user_data = [], array $account_data = [], array $actioned_notes = [], bool $collect_payout_requirements = false, ?string $referral_code = null ): array { $request_args = apply_filters( 'wc_payments_get_onboarding_data_args', [ 'return_url' => $return_url, 'site_data' => $site_data, 'user_data' => $user_data, 'account_data' => $account_data, 'actioned_notes' => $actioned_notes, 'create_live_account' => $live_account, 'collect_payout_requirements' => $collect_payout_requirements, ] ); $request_args['referral_code'] = $referral_code; return $this->request( $request_args, self::ONBOARDING_API . '/init', self::POST, true, true ); } /** * Initialize the onboarding embedded KYC flow, returning a session object which is used by the frontend. * * @param bool $live_account Whether to create live account. * @param array $site_data Site data. * @param array $user_data User data. * @param array $account_data Account data to be prefilled. * @param array $actioned_notes Actioned notes to be sent. * @param ?string $referral_code Referral code to be used for onboarding. * * @return array * * @throws API_Exception */ public function initialize_onboarding_embedded_kyc( bool $live_account, array $site_data = [], array $user_data = [], array $account_data = [], array $actioned_notes = [], ?string $referral_code = null ): array { $request_args = apply_filters( 'wc_payments_get_onboarding_data_args', [ 'site_data' => $site_data, 'user_data' => $user_data, 'account_data' => $account_data, 'actioned_notes' => $actioned_notes, 'create_live_account' => $live_account, ] ); $request_args['referral_code'] = $referral_code; $session = $this->request( $request_args, self::ONBOARDING_API . '/embedded', self::POST, true, true ); if ( ! is_array( $session ) ) { WC_Payments_Utils::log_to_wc( sprintf( 'Failed to initialize embedded KYC: Invalid API response type %s.', gettype( $session ) ) ); return []; } // Log a warning if the session is missing critical fields that indicate a server-side issue. if ( empty( $session['publishable_key'] ) || empty( $session['client_secret'] ) ) { WC_Payments_Utils::log_to_wc( sprintf( 'Embedded KYC session missing required fields: publishable_key=%s, client_secret=%s.', empty( $session['publishable_key'] ) ? 'missing' : 'set', empty( $session['client_secret'] ) ? 'missing' : 'set' ), 'warning' ); } return $session; } /** * Fetch the embedded account session object utilized by the frontend. * * @return array * * @throws API_Exception */ public function create_embedded_account_session(): array { $session = $this->request( [], self::ACCOUNTS_API . '/embedded/session', self::POST, true, true ); if ( ! is_array( $session ) ) { return []; } return $session; } /** * Finalize the onboarding embedded KYC flow. * * @param string $locale The locale to use to i18n the data. * @param string $source The source of the onboarding flow. * @param array $actioned_notes The actioned notes on the account related to this onboarding. * @return array * * @throws API_Exception */ public function finalize_onboarding_embedded_kyc( string $locale, string $source, array $actioned_notes ): array { $request_args = [ 'locale' => $locale, 'source' => $source, 'actioned_notes' => $actioned_notes, ]; return $this->request( $request_args, self::ONBOARDING_API . '/embedded/finalize', self::POST, true, true ); } /** * Get the fields data to be used by the onboarding flow. * * @param string $locale The locale to ask for from the server. * * @return array An array containing the fields data. * * @throws API_Exception Exception thrown on request failure. */ public function get_onboarding_fields_data( string $locale = '' ): array { $fields_data = $this->request( [ 'locale' => $locale, 'test_mode' => WC_Payments::mode()->is_test(), ], self::ONBOARDING_API . '/fields_data', self::GET, false, true ); if ( ! is_array( $fields_data ) ) { throw new API_Exception( __( 'Onboarding field data could not be retrieved', 'woocommerce-payments' ), 'wcpay_onboarding_fields_data_error', 400 ); } return $fields_data; } /** * Get the business types, needed for our KYC onboarding flow. * * @return array An array containing the business types. * * @throws API_Exception Exception thrown on request failure. */ public function get_onboarding_business_types(): array { $business_types = $this->request( [], self::ONBOARDING_API . '/business_types', self::GET, true, true ); if ( ! is_array( $business_types ) ) { return []; } return $business_types; } /** * Get a link's details from the server. * * @param array $args The arguments to be sent with the link request. * * @return array The link object with an url field. * * @throws API_Exception When something goes wrong with the request, when type is not defined or valid, or the link is not valid. */ public function get_link( array $args ) { if ( ! isset( $args['type'] ) && ! in_array( $args['type'], [ 'login_link', 'complete_kyc_link' ], true ) ) { throw new API_Exception( __( 'Link type is required', 'woocommerce-payments' ), 'wcpay_unknown_link_type', 400 ); } return $this->request( $args, self::LINKS_API, self::POST, true, true ); } /** * Create a customer. * * @param array $customer_data Customer data. * * @return string The created customer's ID * * @throws API_Exception Error creating customer. */ public function create_customer( array $customer_data ): string { $customer_array = $this->request( $customer_data, self::CUSTOMERS_API, self::POST ); return $customer_array['id']; } /** * Update a customer. * * @param string $customer_id ID of customer to update. * @param array $customer_data Data to be updated. * * @throws API_Exception Error updating customer. */ public function update_customer( $customer_id, $customer_data = [] ) { if ( null === $customer_id || '' === trim( $customer_id ) ) { throw new API_Exception( __( 'Customer ID is required', 'woocommerce-payments' ), 'wcpay_mandatory_customer_id_missing', 400 ); } if ( ! preg_match( '/^\w+$/', $customer_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } $this->request( $customer_data, self::CUSTOMERS_API . '/' . $customer_id, self::POST ); } /** * Fetch a product. * * @param string $product_id ID of the product to get. * * @return array The product. * * @throws API_Exception If fetching the product fails. */ public function get_product_by_id( string $product_id ): array { return $this->request( [], self::PRODUCTS_API . '/' . $product_id, self::GET ); } /** * Create a product. * * @param array $product_data Product data. * * @return array The created product's product and price IDs. * * @throws API_Exception Error creating the product. */ public function create_product( array $product_data ): array { return $this->request( $product_data, self::PRODUCTS_API, self::POST ); } /** * Update a product. * * @param string $product_id ID of product to update. * @param array $product_data Data to be updated. * * @return array The updated product's product and/or price IDs. * * @throws API_Exception Error updating product. */ public function update_product( string $product_id, array $product_data = [] ): array { if ( null === $product_id || '' === trim( $product_id ) ) { throw new API_Exception( __( 'Product ID is required', 'woocommerce-payments' ), 'wcpay_mandatory_product_id_missing', 400 ); } if ( ! preg_match( '/^\w+$/', $product_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( $product_data, self::PRODUCTS_API . '/' . $product_id, self::POST ); } /** * Update a price. * * @param string $price_id ID of price to update. * @param array $price_data Data to be updated. * * @throws API_Exception Error updating price. */ public function update_price( string $price_id, array $price_data = [] ) { if ( null === $price_id || '' === trim( $price_id ) ) { throw new API_Exception( __( 'Price ID is required', 'woocommerce-payments' ), 'wcpay_mandatory_price_id_missing', 400 ); } if ( ! preg_match( '/^\w+$/', $price_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } $this->request( $price_data, self::PRICES_API . '/' . $price_id, self::POST ); } /** * Fetch an invoice. * * @param string $invoice_id ID of the invoice to get. * * @return array The invoice. * * @throws API_Exception If fetching the invoice fails. */ public function get_invoice( string $invoice_id ) { if ( ! preg_match( '/^\w+$/', $invoice_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [], self::INVOICES_API . '/' . $invoice_id, self::GET ); } /** * Charges an invoice. * * Calling this function charges the customer. Pass the param 'paid_out_of_band' => true to mark the invoice as paid without charging the customer. * * @param string $invoice_id ID of the invoice to charge. * @param array $data Parameters to send to the invoice /pay endpoint. Optional. Default is an empty array. * @return array * * @throws API_Exception Error charging the invoice. */ public function charge_invoice( string $invoice_id, array $data = [] ) { if ( ! preg_match( '/^\w+$/', $invoice_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( $data, self::INVOICES_API . '/' . $invoice_id . '/pay', self::POST ); } /** * Updates an invoice. * * @param string $invoice_id ID of the invoice to update. * @param array $data Parameters to send to the invoice endpoint. Optional. Default is an empty array. * @return array * * @throws API_Exception Error updating the invoice. */ public function update_invoice( string $invoice_id, array $data = [] ) { if ( ! preg_match( '/^\w+$/', $invoice_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( $data, self::INVOICES_API . '/' . $invoice_id, self::POST ); } /** * Updates a charge. * * @param string $charge_id ID of the charge to update. * @param array $data arameters to send to the transaction endpoint. Optional. Default is an empty array. * * @return array * @throws API_Exception */ public function update_charge( string $charge_id, array $data = [] ) { if ( ! preg_match( '/^\w+$/', $charge_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( $data, self::CHARGES_API . '/' . $charge_id, self::POST ); } /** * Fetch a charge by id. * * @param string $charge_id Charge id. * * @return array * @throws API_Exception */ public function get_charge( string $charge_id ) { if ( ! preg_match( '/^\w+$/', $charge_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [], self::CHARGES_API . '/' . $charge_id, self::GET ); } /** * Updates a transaction. * * @param string $transaction_id Transaction id. * @param array $data Data to be updated. * * @return array * @throws API_Exception */ public function update_transaction( string $transaction_id, array $data = [] ) { if ( ! preg_match( '/^\w+$/', $transaction_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( $data, self::TRANSACTIONS_API . '/' . $transaction_id, self::POST ); } /** * Fetch a WCPay subscription. * * @param string $wcpay_subscription_id Data used to create subscription. * * @return array The WCPay subscription. * * @throws API_Exception If fetching the subscription fails. */ public function get_subscription( string $wcpay_subscription_id ) { if ( ! preg_match( '/^\w+$/', $wcpay_subscription_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [], self::SUBSCRIPTIONS_API . '/' . $wcpay_subscription_id, self::GET ); } /** * Create a WCPay subscription. * * @param array $data Data used to create subscription. * * @return array New WCPay subscription. * * @throws API_Exception If creating the subscription fails. */ public function create_subscription( array $data = [] ) { return $this->request( $data, self::SUBSCRIPTIONS_API, self::POST ); } /** * Update a WCPay subscription. * * @param string $wcpay_subscription_id WCPay subscription ID. * @param array $data Update subscription data. * * @return array Updated WCPay subscription response from server. * * @throws API_Exception If updating the WCPay subscription fails. */ public function update_subscription( $wcpay_subscription_id, $data ) { if ( ! preg_match( '/^\w+$/', $wcpay_subscription_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( $data, self::SUBSCRIPTIONS_API . '/' . $wcpay_subscription_id, self::POST ); } /** * Cancel a WC Pay subscription. * * @param string $wcpay_subscription_id WCPay subscription ID. * * @return array Canceled subscription. * * @throws API_Exception If canceling the subscription fails. */ public function cancel_subscription( string $wcpay_subscription_id ) { if ( ! preg_match( '/^\w+$/', $wcpay_subscription_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [], self::SUBSCRIPTIONS_API . '/' . $wcpay_subscription_id, self::DELETE ); } /** * Update a WCPay subscription item. * * @param string $wcpay_subscription_item_id WCPay subscription item ID. * @param array $data Update subscription item data. * * @return array Updated WCPay subscription item response from server. * * @throws API_Exception If updating the WCPay subscription item fails. */ public function update_subscription_item( $wcpay_subscription_item_id, $data ) { if ( ! preg_match( '/^\w+$/', $wcpay_subscription_item_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( $data, self::SUBSCRIPTION_ITEMS_API . '/' . $wcpay_subscription_item_id, self::POST ); } /** * Get payment method details. * * @param string $payment_method_id Payment method ID. * * @return array Payment method details. * * @throws API_Exception If payment method does not exist. */ public function get_payment_method( $payment_method_id ) { if ( ! preg_match( '/^\w+$/', $payment_method_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [], self::PAYMENT_METHODS_API . '/' . $payment_method_id, self::GET ); } /** * Update payment method data. * * @param string $payment_method_id Payment method ID. * @param array $payment_method_data Payment method updated data. * * @return array Payment method details. * * @throws API_Exception If payment method update fails. */ public function update_payment_method( $payment_method_id, $payment_method_data = [] ) { if ( ! preg_match( '/^\w+$/', $payment_method_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( $payment_method_data, self::PAYMENT_METHODS_API . '/' . $payment_method_id, self::POST ); } /** * Get payment methods for customer. * * @param string $customer_id The customer ID. * @param string $type Type of payment methods to fetch. * @param int $limit Amount of items to fetch. * * @return array Payment methods response. * * @throws API_Exception If an error occurs. */ public function get_payment_methods( $customer_id, $type, $limit = 100 ) { if ( ! preg_match( '/^\w+$/', $customer_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [ 'customer' => $customer_id, 'type' => $type, 'limit' => $limit, ], self::PAYMENT_METHODS_API, self::GET ); } /** * Detach a payment method from a customer. * * @param string $payment_method_id Payment method ID. * * @return array Payment method details. * * @throws API_Exception If detachment fails. */ public function detach_payment_method( $payment_method_id ) { if ( ! preg_match( '/^\w+$/', $payment_method_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [], self::PAYMENT_METHODS_API . '/' . $payment_method_id . '/detach', self::POST ); } /** * Track a order creation/update event. * * @param array $order_data The order data, as an array. * @param bool $update Is this an update event? (Defaults to false, which is a creation event). * * @return array An array, containing a `success` flag. * * @throws API_Exception If an error occurs. */ public function track_order( $order_data, $update = false ) { return $this->request( [ 'order_data' => $order_data, 'update' => $update, ], self::TRACKING_API . '/order', self::POST ); } /** * Link the current customer with the browsing session, for tracking purposes. * * @param string $session_id Session ID, specific to this site. * @param string $customer_id Stripe customer ID. * * @return array An array, containing a `success` flag. * * @throws API_Exception If an error occurs. */ public function link_session_to_customer( $session_id, $customer_id ) { return $this->request( [ 'session' => $session_id, 'customer' => $customer_id, ], self::TRACKING_API . '/link-session', self::POST ); } /** * Registers a Payment Method Domain. * * @param string $domain_name Domain name which to register for the account. * * @return array An array containing an id and the bool property 'enabled' indicating * whether the domain is enabled for the account. Each Payment Method * (apple_pay, google_pay, link, paypal) in the array have a 'status' * property with the possible values 'active' and 'inactive'. * * @throws API_Exception If an error occurs. */ public function register_domain( $domain_name ) { return $this->request( [ 'domain_name' => $domain_name, // The value needs to be a string. // If it's a boolean, it gets serialized as an integer (1), causing an invalid request error. 'enabled' => 'true', ], self::DOMAIN_REGISTRATION_API, self::POST ); } /** * Registers a card reader to a terminal. * * @param string $location The location to assign the reader to. * @param string $registration_code A code generated by the reader used for registering to an account. * @param ?string $label Custom label given to the reader for easier identification. * If no label is specified, the registration code will be used. * @param ?array $metadata Set of key-value pairs that you can attach to the reader. * Useful for storing additional information about the object. * * @return array Stripe terminal reader object. * @see https://stripe.com/docs/api/terminal/readers/object * * @throws API_Exception If an error occurs. */ public function register_terminal_reader( string $location, string $registration_code, ?string $label = null, ?array $metadata = null ) { $request = [ 'location' => $location, 'registration_code' => $registration_code, ]; if ( null !== $label ) { $request['label'] = $label; } if ( null !== $metadata ) { $request['metadata'] = $metadata; } return $this->request( $request, self::TERMINAL_READERS_API, self::POST ); } /** * Creates a new terminal location. * * @param string $display_name The display name of the terminal location. * @param array $address { * Address partials. * * @type string $country Two-letter country code. * @type string $line1 Address line 1. * @type string $line2 Optional. Address line 2. * @type string $city Optional. City, district, suburb, town, or village. * @type int $postal_code Optional. ZIP or postal code. * @type string $state Optional. State, county, province, or region. * } * @param mixed[] $metadata Optional. Metadata for the location. * * @return array A Stripe terminal location object. * @see https://stripe.com/docs/api/terminal/locations/object * * @throws API_Exception If an error occurs. */ public function create_terminal_location( $display_name, $address, $metadata = [] ) { if ( ! isset( $address['country'], $address['line1'] ) ) { throw new API_Exception( __( 'Address country and line 1 are required.', 'woocommerce-payments' ), 'wcpay_invalid_terminal_location_request', 400 ); } $request = [ 'display_name' => $display_name, 'address' => $address, 'metadata' => $metadata, ]; return $this->request( $request, self::TERMINAL_LOCATIONS_API, self::POST ); } /** * Updates an existing terminal location. * * @param string $location_id The id of the terminal location. * @param string $display_name The display name of the terminal location. * @param array $address { * Address partials. * * @type string $country Two-letter country code. * @type string $line1 Address line 1. * @type string $line2 Optional. Address line 2. * @type string $city Optional. City, district, suburb, town, or village. * @type int $postal_code Optional. ZIP or postal code. * @type string $state Optional. State, county, province, or region. * } * * @return array A Stripe terminal location object. * @see https://stripe.com/docs/api/terminal/locations/object * * @throws API_Exception If an error occurs. */ public function update_terminal_location( $location_id, $display_name, $address ) { if ( ! preg_match( '/^\w+$/', $location_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } // Any parameters not provided will be left unchanged so pass only supplied values. $update_request_body = array_merge( ( isset( $address ) ? [ 'address' => $address ] : [] ), ( isset( $display_name ) ? [ 'display_name' => $display_name ] : [] ) ); return $this->request( $update_request_body, self::TERMINAL_LOCATIONS_API . '/' . $location_id, self::POST ); } /** * Deletes the specified location object. * * @param string $location_id The id of the terminal location. * * @return array Stripe's terminal deletion response. * @see https://stripe.com/docs/api/terminal/locations/delete * * @throws API_Exception If the location id is invalid or downstream call fails. */ public function delete_terminal_location( $location_id ) { if ( ! preg_match( '/^\w+$/', $location_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [], self::TERMINAL_LOCATIONS_API . '/' . $location_id, self::DELETE ); } /** * Retrieves the list of failed webhook events and their data. * * @return array List of failed webhook events. * * @throws API_Exception If an error occurs. */ public function get_failed_webhook_events() { return $this->request( [], self::WEBHOOK_FETCH_API, self::POST ); } /** * Get summary of documents. * * @param array $filters The filters to be used in the query. * * @return array * @throws API_Exception - Exception thrown on request failure. */ public function get_documents_summary( array $filters = [] ) { return $this->request( $filters, self::DOCUMENTS_API . '/summary', self::GET ); } /** * Request a document from the server and returns the full response. * * @param string $document_id The document's ID. * * @return array HTTP response on success. * * @throws API_Exception - If not connected or request failed. */ public function get_document( $document_id ) { if ( ! preg_match( '/^[\w-]+$/', $document_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [], self::DOCUMENTS_API . '/' . $document_id, self::GET, true, false, true ); } /** * Saves the VAT details on the server and returns the full response. * * @param string $vat_number The VAT number. * @param string $name The company's name. * @param string $address The company's address. * * @return array HTTP response on success. * * @throws API_Exception - If not connected or request failed. */ public function save_vat_details( $vat_number, $name, $address ) { $response = $this->request( [ 'vat_number' => $vat_number, 'name' => $name, 'address' => $address, ], self::VAT_API, self::POST ); WC_Payments::get_account_service()->refresh_account_data(); return $response; } /** * Saves the ruleset config as the latest one for the account. * * @param array $ruleset_config The ruleset array. * * @return array HTTP resposne on success. * * @throws API_Exception - If not connected or request failed. */ public function save_fraud_ruleset( $ruleset_config ) { $response = $this->request( [ 'ruleset_config' => $ruleset_config, ], self::FRAUD_RULESET_API, self::POST ); return $response; } /** * Get the latest fraud ruleset for the account. * * @return array HTTP resposne on success. * * @throws API_Exception - If not connected or request failed. */ public function get_latest_fraud_ruleset() { $response = $this->request( [], self::FRAUD_RULESET_API, self::GET ); return $response; } /** * Gets the latest fraud outcome for a given payment intent id. * * @param string $id Payment intent id. * * @throws API_Exception - If not connected or request failed. * * @return array The response object. */ public function get_latest_fraud_outcome( $id ) { if ( ! preg_match( '/^\w+$/', $id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } $response = $this->request( [], self::FRAUD_OUTCOMES_API . '/order_id/' . $id, self::GET ); if ( is_array( $response ) && count( $response ) > 0 ) { return $response[0]; } return $response; } /** * Sends the compatibility data to the server to be saved to the account. * * @param array $compatibility_data The array containing the data. * * @return array HTTP response on success. * * @throws API_Exception - If not connected or request failed. */ public function update_compatibility_data( $compatibility_data ) { $response = $this->request( [ 'compatibility_data' => $compatibility_data, ], self::COMPATIBILITY_API, self::POST ); return $response; } /** * Get tracking info for the site. * * @return array Tracking info. * * @throws API_Exception - If not connected or request failed. */ public function get_tracking_info() { return $this->request( [], self::TRACKING_API . '/info', self::GET, ); } /** * Get the address autocomplete token. * * @return array The address autocomplete token. * * @throws API_Exception - If not connected or request failed. */ public function get_address_autocomplete_token() { return $this->request( [], self::ADDRESS_AUTOCOMPLETE_TOKEN, self::POST, ); } /** * Sends a request object. * * @param Request $request The request to send. * @return array A response object. */ public function send_request( Request $request ) { return $this->request( $request->get_params(), $request->get_api(), $request->get_method(), $request->is_site_specific(), $request->should_use_user_token(), $request->should_return_raw_response() ); } /** * Adds additional info to charge object. * * @param array $charge - Charge object. * * @return array */ public function add_additional_info_to_charge( array $charge ): array { $charge = $this->add_order_info_to_charge_object( $charge['id'], $charge ); $charge = $this->add_formatted_address_to_charge_object( $charge ); return $charge; } /** * Adds the formatted address to the Charge object * * @param array $charge - Charge object. * * @return array */ public function add_formatted_address_to_charge_object( array $charge ): array { $has_billing_details = isset( $charge['billing_details'] ); if ( $has_billing_details ) { $raw_details = $charge['billing_details']['address']; $billing_details = []; $billing_details['city'] = ( ! empty( $raw_details['city'] ) ) ? $raw_details['city'] : ''; $billing_details['country'] = ( ! empty( $raw_details['country'] ) ) ? $raw_details['country'] : ''; $billing_details['address_1'] = ( ! empty( $raw_details['line1'] ) ) ? $raw_details['line1'] : ''; $billing_details['address_2'] = ( ! empty( $raw_details['line2'] ) ) ? $raw_details['line2'] : ''; $billing_details['postcode'] = ( ! empty( $raw_details['postal_code'] ) ) ? $raw_details['postal_code'] : ''; $billing_details['state'] = ( ! empty( $raw_details['state'] ) ) ? $raw_details['state'] : ''; $charge['billing_details']['formatted_address'] = WC()->countries->get_formatted_address( $billing_details ); } return $charge; } /** * Creates the array representing order for frontend. * * @param WC_Order $order The order. * @return array */ public function build_order_info( WC_Order $order ): array { $order_info = [ 'id' => $order->get_id(), 'number' => $order->get_order_number(), 'url' => $order->get_edit_order_url(), 'customer_url' => $this->get_customer_url( $order ), 'customer_name' => trim( $order->get_formatted_billing_full_name() ), 'customer_email' => $order->get_billing_email(), 'fraud_meta_box_type' => $order->get_meta( '_wcpay_fraud_meta_box_type' ), 'ip_address' => $order->get_customer_ip_address(), 'suggested_product_type' => $this->determine_suggested_product_type( $order ), ]; if ( function_exists( 'wcs_get_subscriptions_for_order' ) ) { $order_info['subscriptions'] = []; $subscriptions = wcs_get_subscriptions_for_order( $order, [ 'order_type' => [ 'parent', 'renewal' ] ] ); foreach ( $subscriptions as $subscription ) { $order_info['subscriptions'][] = [ 'number' => $subscription->get_order_number(), 'url' => $subscription->get_edit_order_url(), ]; } } return $order_info; } /** * De-serialize an intention array into a payment intention object * * @param array $intention_array - The intention array to de-serialize. * * @return WC_Payments_API_Payment_Intention * @throws API_Exception - Unable to deserialize intention array. */ public function deserialize_payment_intention_object_from_array( array $intention_array ) { // TODO: Throw an exception if the response array doesn't contain mandatory properties. $created = new DateTime(); $created->setTimestamp( $intention_array['created'] ); // Metadata can be an empty stdClass object, so we need to check array type too. // See https://github.com/Automattic/woocommerce-payments/pull/419/commits/c2c8438c3ed7be6d604435e059209fb87fb6d0c4. $raw_metadata = $intention_array['metadata']; $metadata = is_array( $raw_metadata ) && ! empty( $raw_metadata ) ? $raw_metadata : []; $charge_array = 0 < $intention_array['charges']['total_count'] ? end( $intention_array['charges']['data'] ) : null; $next_action = $intention_array['next_action'] ?? []; $last_payment_error = $intention_array['last_payment_error'] ?? []; $customer = $intention_array['customer'] ?? $charge_array['customer'] ?? null; $payment_method = $intention_array['payment_method'] ?? $intention_array['source'] ?? null; $processing = $intention_array[ Intent_Status::PROCESSING ] ?? []; $payment_method_types = $intention_array['payment_method_types'] ?? []; $payment_method_options = $intention_array['payment_method_options'] ?? []; $charge = ! empty( $charge_array ) ? self::deserialize_charge_object_from_array( $charge_array ) : null; $order = $this->get_order_info_from_intention_object( $intention_array['id'], $intention_array['metadata']['order_key'] ?? null ); $intent = new WC_Payments_API_Payment_Intention( $intention_array['id'], $intention_array['amount'], $intention_array['currency'], $customer, $payment_method, $created, $intention_array['status'], $intention_array['client_secret'], $charge, $next_action, $last_payment_error, $metadata, $processing, $payment_method_types, $payment_method_options, $order ); return $intent; } /** * De-serialize an intention array into a setup intention object * * @param array $intention_array - The intention array to de-serialize. * * @return WC_Payments_API_Setup_Intention * @throws API_Exception - Unable to deserialize intention array. */ public function deserialize_setup_intention_object_from_array( array $intention_array ): WC_Payments_API_Setup_Intention { $created = new DateTime(); $created->setTimestamp( $intention_array['created'] ); // Metadata can be an empty stdClass object, so we need to check array type too. // See https://github.com/Automattic/woocommerce-payments/pull/419/commits/c2c8438c3ed7be6d604435e059209fb87fb6d0c4. $raw_metadata = $intention_array['metadata']; $metadata = is_array( $raw_metadata ) && ! empty( $raw_metadata ) ? $raw_metadata : []; $next_action = $intention_array['next_action'] ?? []; $last_setup_error = $intention_array['last_setup_error'] ?? []; $customer = $intention_array['customer'] ?? null; $payment_method = $intention_array['payment_method'] ?? $intention_array['source'] ?? null; $payment_method_types = $intention_array['payment_method_types'] ?? []; $payment_method_options = $intention_array['payment_method_options'] ?? []; $order = $this->get_order_info_from_intention_object( $intention_array['id'] ); $intent = new WC_Payments_API_Setup_Intention( $intention_array['id'], $customer, $payment_method, $created, $intention_array['status'], $intention_array['client_secret'], $next_action, $last_setup_error, $metadata, $payment_method_types, $payment_method_options, $order ); return $intent; } /** * Fetch readers charge summary. * * @param string $charge_date Charge date for readers. * @param string|null $transaction_id Optional transaction ID to filter results. * * @return array reader objects. */ public function get_readers_charge_summary( string $charge_date, ?string $transaction_id = null ): array { $params = [ 'charge_date' => $charge_date ]; if ( $transaction_id ) { $params['transaction_id'] = $transaction_id; } return $this->request( $params, self::READERS_CHARGE_SUMMARY, self::GET ); } /** * Fetches from the server the minimum amount that can be processed in recurring transactions for a given currency. * * @param string $currency The currency code. * * @return int The minimum amount that can be processed in cents (with no decimals). * * @throws API_Exception If an error occurs. */ public function get_currency_minimum_recurring_amount( $currency ) { if ( ! preg_match( '/^\w+$/', $currency ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return (int) $this->request( [], self::MINIMUM_RECURRING_AMOUNT_API . '/' . $currency, self::GET ); } /** * Return summary for authorizations. * * @return array The authorizations summary. * @throws API_Exception Exception thrown on request failure. */ public function get_authorizations_summary() { return $this->request( [], self::AUTHORIZATIONS_API . '/summary', self::GET ); } /** * Fetch a single authorizations with provided payment intent id. * * @param string $payment_intent_id id of requested transaction. * @return array authorization object. * @throws API_Exception - Exception thrown in case route validation fails. */ public function get_authorization( string $payment_intent_id ) { if ( ! preg_match( '/^\w+$/', $payment_intent_id ) ) { throw new API_Exception( __( 'Route param validation failed.', 'woocommerce-payments' ), 'wcpay_route_validation_failure', 400 ); } return $this->request( [], self::AUTHORIZATIONS_API . '/' . $payment_intent_id, self::GET ); } /** * Gets the WooPay compatibility list. * * @return array of incompatible extensions, adapted extensions and available countries. * @throws API_Exception When request fails. */ public function get_woopay_compatibility() { return $this->request( [], self::WOOPAY_COMPATIBILITY_API, self::GET, false ); } /** * Delete account. * * @param bool $test_mode Whether we are in test mode or not. * * @return array * @throws API_Exception */ public function delete_account( bool $test_mode = false ) { return $this->request( [ 'test_mode' => $test_mode, ], self::ACCOUNTS_API . '/delete', self::POST, true, true ); } /** * Send store setup data to the Transact Platform. * * Use a non-blocking request as this is not critical data, and it should have minimal impact on the user experience. * * @param array $store_setup The store setup data. * * @return array Response from the API. * @throws API_Exception */ public function send_store_setup( array $store_setup ): array { return $this->request( [ 'snapshot' => $store_setup, 'test_mode' => \WC_Payments::mode()->is_test_mode_onboarding(), ], self::STORE_SETUP_API, self::POST, true, false, false, false, false ); } /** * Send the request to the WooCommerce Payment API * * @param array $params - Request parameters to send as either JSON or GET string. Defaults to test_mode=1 if either in dev or test mode, 0 otherwise. * @param string $api - The API endpoint to call. * @param string $method - The HTTP method to make the request with. * @param bool $is_site_specific - If true, the site ID will be included in the request url. Defaults to true. * @param bool $use_user_token - If true, the request will be signed with the user token rather than blog token. Defaults to false. * @param bool $raw_response - If true, the raw response will be returned. Defaults to false. * @param bool $use_v2_api - If true, the request will be sent to the V2 API endpoint. Defaults to false. * @param bool $blocking - If true, the request will be blocking. Defaults to true. * * @return array * @throws API_Exception - If the account ID hasn't been set. */ protected function request( $params, $api, $method, $is_site_specific = true, $use_user_token = false, bool $raw_response = false, bool $use_v2_api = false, bool $blocking = true ) { // Apply the default params that can be overridden by the calling method. $params = wp_parse_args( $params, [ 'test_mode' => WC_Payments::mode()->is_test(), ] ); $params = apply_filters( 'wcpay_api_request_params', $params, $api, $method ); // Build the URL we want to send the request to. $url = self::ENDPOINT_BASE; if ( $is_site_specific ) { $url .= '/' . self::ENDPOINT_SITE_FRAGMENT; } if ( $use_v2_api ) { $url .= '/' . self::V2_ENDPOINT_REST_BASE; } else { $url .= '/' . self::ENDPOINT_REST_BASE; } $url .= '/' . $api; $headers = []; $headers['Content-Type'] = 'application/json; charset=utf-8'; $headers['User-Agent'] = $this->user_agent; $body = null; $redacted_params = WC_Payments_Utils::redact_array( $params, self::API_KEYS_TO_REDACT ); $redacted_url = $url; if ( in_array( $method, [ self::GET, self::DELETE ], true ) ) { $url .= '?' . http_build_query( $params ); $redacted_url .= '?' . http_build_query( $redacted_params ); } else { $headers['Idempotency-Key'] = $this->uuid(); $body = wp_json_encode( $params ); if ( ! $body ) { throw new API_Exception( __( 'Unable to encode body for request to WooCommerce Payments API.', 'woocommerce-payments' ), 'wcpay_client_unable_to_encode_json', 0 ); } } $headers = apply_filters( 'wcpay_api_request_headers', $headers ); $stop_trying_at = time() + self::API_TIMEOUT_SECONDS; $retries = 0; $retries_limit = array_key_exists( 'Idempotency-Key', $headers ) ? self::API_RETRIES_LIMIT : 0; while ( true ) { $response_code = null; $last_exception = null; // The header intention is to give us insights into request latency between store and backend. $headers['X-Request-Initiated'] = microtime( true ); $request_args = [ 'url' => $url, 'method' => $method, 'headers' => $headers, 'timeout' => self::API_TIMEOUT_SECONDS, 'connect_timeout' => self::API_TIMEOUT_SECONDS, 'blocking' => $blocking, ]; $log_request_id = uniqid(); Logger::info( sprintf( 'API REQUEST (%s): %s %s', $log_request_id, $method, $redacted_url ), [ 'request' => $request_args, null !== $body ? [ 'body' => $redacted_params ] : [], ] ); try { $response = $this->http_client->remote_request( $request_args, $body, $is_site_specific, $use_user_token ); $response = apply_filters( 'wcpay_api_request_response', $response, $method, $url, $api ); $response_code = wp_remote_retrieve_response_code( $response ); $this->check_response_for_errors( $response ); } catch ( Connection_Exception $e ) { $last_exception = $e; } if ( $response_code || time() >= $stop_trying_at || $retries_limit === $retries ) { if ( null !== $last_exception ) { throw $last_exception; } break; } // Use exponential backoff to not overload backend. usleep( self::API_RETRIES_BACKOFF_MSEC * ( 2 ** $retries ) ); ++$retries; } // @todo We don't always return an array. `extract_response_body` can also return a string. We should standardize this! if ( ! $raw_response ) { $response_body = $this->extract_response_body( $response ); } else { $response_body = $response; } Logger::info( sprintf( 'API RESPONSE (%s): %s %s', $log_request_id, $method, $redacted_url ), [ 'body' => WC_Payments_Utils::redact_array( $response_body, self::API_KEYS_TO_REDACT ), ] ); return $response_body; } /** * From a given response extract the body. * * @param array $response That was given to us by http_client remote_request. * * @return mixed $response_body */ protected function extract_response_body( $response ) { $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( null === $response_body ) { return wp_remote_retrieve_body( $response ); } // Make sure empty metadata serialized on the client as an empty object {} rather than array []. if ( isset( $response_body['metadata'] ) && empty( $response_body['metadata'] ) ) { $response_body['metadata'] = new stdClass(); } return $response_body; } /** * Checks if a response has any errors and throws the appropriate API_Exception. * * @param array $response That was given to us by http_client remote_request. * * @return void * * @throws API_Exception If there's something wrong with the response. */ protected function check_response_for_errors( $response ) { $response_code = wp_remote_retrieve_response_code( $response ); if ( ! $response_code ) { $response_code = 0; } $response_body_json = wp_remote_retrieve_body( $response ); $response_body = json_decode( $response_body_json, true ); if ( null === $response_body && $this->is_json_response( $response ) ) { $message = __( 'Unable to decode response from WooCommerce Payments API', 'woocommerce-payments' ); Logger::error( $message ); throw new API_Exception( $message, 'wcpay_unparseable_or_null_body', $response_code ); } elseif ( null === $response_body && ! $this->is_json_response( $response ) ) { $response_body = wp_remote_retrieve_body( $response ); } // Check error codes for 4xx and 5xx responses. if ( 400 <= $response_code ) { $error_type = null; $decline_code = null; if ( isset( $response_body['code'] ) && 'amount_too_small' === $response_body['code'] ) { throw new Amount_Too_Small_Exception( $response_body['message'], $response_body['data']['minimum_amount'], $response_body['data']['currency'], $response_code ); } elseif ( isset( $response_body['error'] ) ) { $response_body_error_code = $response_body['error']['code'] ?? $response_body['error']['message_code'] ?? null; $payment_intent_status = $response_body['error']['payment_intent']['status'] ?? null; // We redact the API error message to prevent prompting the merchant to contact Stripe support // when attempting to manually capture an amount greater than what's authorized. Contacting support is unnecessary in this scenario. if ( 'amount_too_large' === $response_body_error_code && Intent_Status::REQUIRES_CAPTURE === $payment_intent_status ) { throw new Amount_Too_Large_Exception( // translators: This is an error API response. __( 'Error: The payment could not be captured because the requested capture amount is greater than the amount you can capture for this charge.', 'woocommerce-payments' ), $response_code ); } $decline_code = $response_body['error']['decline_code'] ?? ''; $this->maybe_act_on_fraud_prevention( $decline_code ); $error_code = $response_body_error_code ?? $response_body['error']['type'] ?? null; $error_message = $response_body['error']['message'] ?? null; $error_type = $response_body['error']['type'] ?? null; } elseif ( isset( $response_body['code'] ) ) { $this->maybe_act_on_fraud_prevention( $response_body['code'] ); if ( 'invalid_request_error' === $response_body['code'] && 0 === strpos( $response_body['message'], 'You cannot combine currencies on a single customer.' ) ) { // Get the currency, which is the last part of the error message, // and remove the period from the end of the error message. $message = $response_body['message']; $currency = substr( $message, -4 ); $currency = strtoupper( substr( $currency, 0, 3 ) ); // Only throw the error if we can find a valid currency. if ( false !== Currency_Code::search( $currency ) ) { throw new Cannot_Combine_Currencies_Exception( $message, $currency, $response_code ); } } $error_code = $response_body['code']; $error_message = $response_body['message']; } else { $error_code = 'wcpay_client_error_code_missing'; $error_message = __( 'Server error. Please try again.', 'woocommerce-payments' ); } $message = sprintf( // translators: This is an error API response. _x( 'Error: %1$s', 'API error message to throw as Exception', 'woocommerce-payments' ), $error_message ); Logger::error( "$error_message ($error_code)" ); if ( 'card_declined' === $error_code && isset( $response_body['error']['payment_intent']['charges']['data'][0]['outcome']['seller_message'] ) ) { $merchant_message = $response_body['error']['payment_intent']['charges']['data'][0]['outcome']['seller_message']; throw new API_Merchant_Exception( $message, $error_code, $response_code, $merchant_message, $error_type, $decline_code ); } throw new API_Exception( $message, $error_code, $response_code, $error_type, $decline_code ); } } /** * Returns true if the response is JSON, based on the content-type header. * * @param array $response That was given to us by http_client remote_request. * * @return bool True if content-type is application/json, false otherwise. */ protected function is_json_response( $response ) { return 'application/json' === substr( wp_remote_retrieve_header( $response, 'content-type' ), 0, strlen( 'application/json' ) ); } /** * If error code indicates fraudulent activity, trigger fraud prevention measures. * * @param string $error_code Error code. * * @return void */ private function maybe_act_on_fraud_prevention( string $error_code ) { // Might be flagged by Stripe Radar or WCPay card testing prevention services. $is_fraudulent = 'fraudulent' === $error_code || 'wcpay_card_testing_prevention' === $error_code; if ( $is_fraudulent && WC()->session ) { $fraud_prevention_service = Fraud_Prevention_Service::get_instance(); if ( $fraud_prevention_service->is_enabled() ) { $fraud_prevention_service->regenerate_token(); // Here we tried triggering checkout refresh, but it clashes with AJAX handling. } } } /** * Adds additional info to intention object. * * @param string $intention_id Intention ID. * @param string $order_key Order key. * @return array */ private function get_order_info_from_intention_object( $intention_id, $order_key = null ) { $order = $this->wcpay_db->order_from_intent_id( $intention_id ); if ( $order instanceof WC_Order && null !== $order_key && $order_key !== $order->get_order_key() ) { return []; } $object = $this->add_order_info_to_object( $order, [] ); return $object['order']; } /** * Adds order information to the charge object. * * @param string $charge_id Charge ID. * @param array $entity Object to add order information. * * @return array */ private function add_order_info_to_charge_object( $charge_id, $entity ) { $order = $this->wcpay_db->order_from_charge_id( $charge_id ); $entity = $this->add_order_info_to_object( $order, $entity ); return $entity; } /** * Returns a transaction with order information when it exists. * * @param bool|\WC_Order|\WC_Order_Refund $order Order object. * @param array $entity Object to add order information. * * @return array new object with order information. */ private function add_order_info_to_object( $order, $entity ) { // Add order information to the `$transaction`. // If the order couldn't be retrieved, return an empty order. $entity['order'] = []; if ( $order ) { $entity['order'] = $this->build_order_info( $order ); } return $entity; } /** * Generates url to single customer in analytics table. * * @param WC_Order $order The Order. * @return string|null */ private function get_customer_url( WC_Order $order ) { $customer_id = DataStore::get_existing_customer_id_from_order( $order ); if ( ! $customer_id ) { return null; } return add_query_arg( [ 'page' => 'wc-admin', 'path' => '/customers', 'filter' => 'single_customer', 'customers' => $customer_id, ], 'admin.php' ); } /** * De-serialize a charge array into a charge object * * @param array $charge_array - The charge array to de-serialize. * * @return WC_Payments_API_Charge * @throws API_Exception - Unable to deserialize charge array. */ private function deserialize_charge_object_from_array( array $charge_array ) { // TODO: Throw an exception if the response array doesn't contain mandatory properties. $created = new DateTime(); $created->setTimestamp( $charge_array['created'] ); $charge_array = $this->add_additional_info_to_charge( $charge_array ); $charge = new WC_Payments_API_Charge( $charge_array['id'], $charge_array['amount'], $created, $charge_array['payment_method_details'] ?? null, $charge_array['payment_method'] ?? null, $charge_array['amount_captured'] ?? null, $charge_array['amount_refunded'] ?? null, $charge_array['application_fee_amount'] ?? null, $charge_array['balance_transaction'] ?? null, $charge_array['billing_details'] ?? null, $charge_array['currency'] ?? null, $charge_array['dispute'] ?? null, $charge_array['disputed'] ?? null, $charge_array['order'] ?? null, $charge_array['outcome'] ?? null, $charge_array['paid'] ?? null, $charge_array['paydown'] ?? null, $charge_array['payment_intent'] ?? null, $charge_array['refunded'] ?? null, $charge_array['refunds'] ?? null, $charge_array['status'] ?? null ); if ( isset( $charge_array['captured'] ) ) { $charge->set_captured( $charge_array['captured'] ); } return $charge; } /** * Returns a formatted intention description. * * @param string $order_number The order number (might be different from the ID). * @return string A formatted intention description. */ private function get_intent_description( $order_number ): string { $domain_name = str_replace( [ 'https://', 'http://' ], '', get_site_url() ); $blog_id = $this->get_blog_id(); // Forgo i18n as this is only visible in the Stripe dashboard. return sprintf( 'Online Payment%s for %s%s', 0 !== $order_number ? " for Order #$order_number" : '', $domain_name, null !== $blog_id ? " blog_id $blog_id" : '' ); } /** * Returns a v4 UUID. * * @return string */ private function uuid() { $arr = array_values( unpack( 'N1a/n4b/N1c', random_bytes( 16 ) ) ); $arr[2] = ( $arr[2] & 0x0fff ) | 0x4000; $arr[3] = ( $arr[3] & 0x3fff ) | 0x8000; return vsprintf( '%08x-%04x-%04x-%04x-%04x%08x', $arr ); } /** * Returns a list of fingerprinting metadata to attach to order. * * @param string $fingerprint User fingerprint. * * @return array List of fingerprinting metadata. * * @throws API_Exception If an error occurs. */ private function get_fingerprint_metadata( $fingerprint = '' ): array { $customer_fingerprint_metadata = Buyer_Fingerprinting_Service::get_instance()->get_hashed_data_for_customer( $fingerprint ); $customer_fingerprint_metadata['fraud_prevention_data_available'] = true; return $customer_fingerprint_metadata; } /** * Determine the suggested product type based on the order's products. * * @param WC_Order $order The order. * @return string The suggested product type. */ private function determine_suggested_product_type( WC_Order $order ): string { $items = $order->get_items(); if ( empty( $items ) ) { return 'physical_product'; } $virtual_products = 0; $product_count = 0; $product_type = null; foreach ( $items as $item ) { // Only process product items. if ( ! $item instanceof WC_Order_Item_Product ) { continue; } $product = $item->get_product(); if ( ! $product ) { continue; } ++$product_count; // Capture first product's type (only used for single-product orders). if ( null === $product_type ) { $product_type = $product->get_type(); } if ( $product->is_virtual() ) { ++$virtual_products; } } // If no valid products found, default to physical. if ( 0 === $product_count ) { return 'physical_product'; } // If more than one product, suggest multiple. if ( $product_count > 1 ) { return 'multiple'; } // At this point, we know there's exactly one product. // Check for specific product types (gated by feature flag). if ( WC_Payments_Features::is_dispute_additional_evidence_types_enabled() && 'booking' === $product_type ) { return 'booking_reservation'; } // Check if it's virtual (digital product or service). if ( 1 === $virtual_products ) { return 'digital_product_or_service'; } // Everything else defaults to physical. return 'physical_product'; } } includes/wc-payment-api/models/class-wc-payments-api-setup-intention.php 0000777 00000006415 15251156605 0022510 0 ustar 00 <?php /** * WC_Payments_API_Setup_Intention class * * @package WooCommerce\Payments */ /** * Setup Intent object used by the WooCommerce Payments API. * * Only add getters and properties existing in setup intent object https://stripe.com/docs/api/setup_intents/object. * Otherwise, add them in the (abstract) parent class. */ class WC_Payments_API_Setup_Intention extends WC_Payments_API_Abstract_Intention { /** * The last payment error of the intention * * @var array */ private $last_setup_error; /** * WC_Payments_API_Setup_Intention constructor. * * @param string $id - ID of the intention. * @param string|null $customer_id - Stripe ID of the customer. * @param string|null $payment_method_id - Stripe ID of the payment method. * @param DateTime $created - Time charge created. * @param string $status - Intention status. * @param string $client_secret - The client secret of the intention. * @param array $next_action - An array containing information for next action to take. * @param array $last_setup_error - An array containing details of any errors. * @param array $metadata - An array containing additional metadata of associated charge or order. * @param array $payment_method_types - An array containing the possible payment methods for the intent. * @param array $payment_method_options - An array containing the payment method options for the intent. * @param array $order - An array containing the order data associated with this intention. */ public function __construct( string $id, ?string $customer_id, ?string $payment_method_id, DateTime $created, string $status, string $client_secret, array $next_action = [], array $last_setup_error = [], array $metadata = [], array $payment_method_types = [], array $payment_method_options = [], array $order = [] ) { $this->id = $id; $this->created = $created; $this->status = $status; $this->client_secret = $client_secret; $this->next_action = $next_action; $this->last_setup_error = $last_setup_error; $this->customer_id = $customer_id; $this->payment_method_id = $payment_method_id; $this->metadata = $metadata; $this->payment_method_types = $payment_method_types; $this->payment_method_options = $payment_method_options; $this->order = $order; } /** * Returns the last payment error of this intention * * @return array */ public function get_last_setup_error(): array { return $this->last_setup_error; } /** * Defines which data will be serialized to JSON */ public function jsonSerialize(): array { return [ 'id' => $this->get_id(), 'created' => $this->get_created()->getTimestamp(), 'customer' => $this->get_customer_id(), 'metadata' => $this->get_metadata(), 'payment_method' => $this->get_payment_method_id(), 'payment_method_types' => $this->get_payment_method_types(), 'status' => $this->get_status(), 'order' => $this->get_order(), ]; } } includes/wc-payment-api/models/class-wc-payments-api-abstract-intention.php 0000777 00000007714 15251156605 0023156 0 ustar 00 <?php /** * WC_Payments_API_Abstract_Intention class * * @package WooCommerce\Payments */ use WCPay\Constants\Intent_Status; /** * An abstract object representing payment and setup intents used by the WooCommerce Payments API. * * Only add shared getters and properties in this class. Otherwise, add them inherited classes. */ abstract class WC_Payments_API_Abstract_Intention implements \JsonSerializable { /** * Intention ID * * @var string */ protected $id; /** * Time charge created * * Server-side times are presumed to be UTC, (de)serializers should take care to set/respect the timezone on the * DateTime object. * * @var DateTime */ protected $created; /** * The status of the intention * * @var string */ protected $status; /** * The client secret of the intention * * @var string */ protected $client_secret; /** * ID of the customer making the payment * * @var string|null */ protected $customer_id; /** * ID of the payment method used. * * @var string|null */ protected $payment_method_id; /** * The next action needed of the intention * * @var array */ protected $next_action; /** * Set of key-value pairs that can be useful for storing * additional information about the object in a structured format. * * @var array */ protected $metadata; /** * The possible payment method types for the payment. * * @var array */ protected $payment_method_types; /** * The order data associated with this intention. * * @var array */ protected $order; /** * The payment method options for the payment. * * @var array */ protected $payment_method_options; /** * Gets charge ID * * @return string */ public function get_id() { return $this->id; } /** * Gets charge created time * * @return DateTime */ public function get_created() { return $this->created; } /** * Gets intention status * * @return string */ public function get_status() { return $this->status; } /** * Returns the client secret associated with this intention * * @return string */ public function get_client_secret() { return $this->client_secret; } /** * Returns the customer ID of this intention * * @return string|null */ public function get_customer_id() { return $this->customer_id; } /** * Returns the payment method ID of this intention * * @return string|null */ public function get_payment_method_id() { return $this->payment_method_id; } /** * Returns the next action of this intention * * @return array */ public function get_next_action() { return $this->next_action; } /** * Returns the metadata associated with this intention * * @return array */ public function get_metadata() { return $this->metadata; } /** * Returns the payment_method_types state of this intention * * @return array */ public function get_payment_method_types() { return $this->payment_method_types; } /** * Returns the order data associated with this intention * * @return array */ public function get_order() { return $this->order; } /** * Returns the payment method options associated with this intention * * @return array */ public function get_payment_method_options() { return $this->payment_method_options; } /** * Return the payment method type used in the intent that is derived from property `payment_method_options`. * * @return string|null */ public function get_payment_method_type() { $keys = count( (array) $this->payment_method_options ) > 0 ? array_keys( $this->payment_method_options ) : null; return $keys ? $keys[0] : null; } /** * Returns whether the intention is in one of the authorized statuses. * * @return bool */ public function is_authorized(): bool { return in_array( $this->status, Intent_Status::AUTHORIZED_STATUSES, true ); } /** * Defines which data will be serialized to JSON */ abstract public function jsonSerialize(): array; } includes/wc-payment-api/models/class-wc-payments-api-payment-intention.php 0000777 00000012101 15251156605 0023012 0 ustar 00 <?php /** * WC_Payments_API_Intention class * * @package WooCommerce\Payments */ /** * Payment Intent object used by the WooCommerce Payments API. * * Only add getters and properties existing in payment intent object https://stripe.com/docs/api/payment_intents/object. * Otherwise, add them in the (abstract) parent class. */ class WC_Payments_API_Payment_Intention extends WC_Payments_API_Abstract_Intention { /** * Charge amount * * @var int */ private $amount; /** * The currency of the intention * * @var string */ private $currency; /** * The last payment error of the intention * * @var array */ private $last_payment_error; /** * The latest charge object * * @var WC_Payments_API_Charge */ private $charge; /** * The details on the state of the payment. * * @var array */ private $processing; /** * WC_Payments_API_Intention constructor. * * @param string $id - ID of the intention. * @param integer $amount - Amount charged. * @param string $currency - The currency of the intention. * @param string|null $customer_id - Stripe ID of the customer. * @param string|null $payment_method_id - Stripe ID of the payment method. * @param DateTime $created - Time charge created. * @param string $status - Intention status. * @param string $client_secret - The client secret of the intention. * @param WC_Payments_API_Charge $charge - An array containing payment method details of associated charge. * @param array $next_action - An array containing information for next action to take. * @param array $last_payment_error - An array containing details of any errors. * @param array $metadata - An array containing additional metadata of associated charge or order. * @param array $processing - An array containing details of the processing state of the payment. * @param array $payment_method_types - An array containing the possible payment methods for the intent. * @param array $payment_method_options - An array containing the payment method options for the intent. * @param array $order - An array containing the order data associated with this intention. */ public function __construct( $id, $amount, string $currency, $customer_id, $payment_method_id, DateTime $created, $status, $client_secret, $charge = null, $next_action = [], $last_payment_error = [], $metadata = [], $processing = [], $payment_method_types = [], $payment_method_options = [], $order = [] ) { $this->id = $id; $this->amount = $amount; $this->created = $created; $this->status = $status; $this->client_secret = $client_secret; $this->currency = strtoupper( $currency ); $this->next_action = $next_action; $this->last_payment_error = $last_payment_error; $this->customer_id = $customer_id; $this->payment_method_id = $payment_method_id; $this->charge = $charge; $this->metadata = $metadata; $this->processing = $processing; $this->payment_method_types = $payment_method_types; $this->payment_method_options = $payment_method_options; $this->order = $order; } /** * Gets charge amount * * @return int */ public function get_amount() { return $this->amount; } /** * Returns the currency of this intention * * @return string */ public function get_currency() { return $this->currency; } /** * Returns the last payment error of this intention * * @return array */ public function get_last_payment_error() { return $this->last_payment_error; } /** * Returns the charge associated with this intention * * @return WC_Payments_API_Charge */ public function get_charge() { return $this->charge; } /** * Returns the processing state of this intention * * @return array */ public function get_processing() { return $this->processing; } /** * Defines which data will be serialized to JSON */ public function jsonSerialize(): array { return [ 'id' => $this->get_id(), 'amount' => $this->get_amount(), 'currency' => $this->get_currency(), 'charge' => $this->get_charge(), 'created' => $this->get_created()->getTimestamp(), 'customer' => $this->get_customer_id(), 'metadata' => $this->get_metadata(), 'payment_method' => $this->get_payment_method_id(), 'payment_method_types' => $this->get_payment_method_types(), 'processing' => $this->get_processing(), 'status' => $this->get_status(), 'order' => $this->get_order(), ]; } } includes/wc-payment-api/models/class-wc-payments-api-charge.php 0000777 00000025140 15251156605 0020570 0 ustar 00 <?php /** * WC_Payments_API_Charge class * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; /** * A charge object used by the WooCommerce Payments API. */ class WC_Payments_API_Charge implements \JsonSerializable { /** * Charge ID * * @var string */ private $id; /** * Charge amount * * @var int */ private $amount; /** * Time charge created * * Server-side times are presumed to be UTC, (de)serializers should take care to set/respect the timezone on the * DateTime object. * * @var DateTime */ private $created; /** * Flag indicated whether the charge has been captured or not * * @var bool */ private $captured; /** * Payment method details object * * @var array */ private $payment_method_details; /** * Payment method id * * @var string|null */ private $payment_method; /** * Amount in cents captured * * @var int|null */ private $amount_captured; /** * Amount in cents refunded * * @var int|null */ private $amount_refunded; /** * The amount of the application fee requested for the charge * * @var int|null */ private $application_fee_amount; /** * Balance transaction that describes the impact of this charge on the account balance * * @var array */ private $balance_transaction; /** * Billing information associated with the payment method at the time of the transaction * * @var array */ private $billing_details; /** * Charge currency * * @var string|null */ private $currency; /** * Charge dispute object * * @var array */ private $dispute; /** * Flag indicating whether the charge has been disputed or not * * @var bool|null */ private $disputed; /** * Charge order object * * @var array */ private $order; /** * Details about whether the payment was accepted, and why * * @var array */ private $outcome; /** * Flag indicating whether the charge has been paid or not * * @var bool|null */ private $paid; /** * Charge paydown object * * @var array */ private $paydown; /** * Charge payment intent id * * @var string|null */ private $payment_intent; /** * Flag indicating whether the charge has been refunded or not * * @var bool|null */ private $refunded; /** * Charge refunds object * * @var array */ private $refunds; /** * Charge status * * @var string|null */ private $status; /** * WC_Payments_API_Charge constructor. * * @param string $id - ID of the charge. * @param integer $amount - Amount charged. * @param DateTime $created - Time charge created. * @param array $payment_method_details - Payment method details object. * @param string|null $payment_method - Payment method id. * @param int|null $amount_captured - Amount in cents captured. * @param int|null $amount_refunded - Amount in cents refunded. * @param int|null $application_fee_amount - The amount of the application fee requested for the charge. * @param array $balance_transaction - Balance transaction that describes the impact of this charge on the account balance. * @param array $billing_details - Billing information associated with the payment method at the time of the transaction. * @param string|null $currency - Charge currency. * @param array $dispute - Charge dispute object. * @param bool|null $disputed - Flag indicating whether the charge has been disputed or not. * @param array $order - Charge order object. * @param array $outcome - Details about whether the payment was accepted, and why. * @param bool|null $paid - Flag indicating whether the charge has been paid or not. * @param array $paydown - Charge paydown object. * @param string|null $payment_intent - Charge payment intent id. * @param bool|null $refunded - Flag indicating whether the charge has been refunded or not. * @param array $refunds - Charge refunds object. * @param string|null $status - Charge status. */ public function __construct( $id, $amount, DateTime $created, $payment_method_details = [], $payment_method = null, $amount_captured = null, $amount_refunded = null, $application_fee_amount = null, $balance_transaction = [], $billing_details = [], $currency = null, $dispute = [], $disputed = null, $order = [], $outcome = [], $paid = null, $paydown = [], $payment_intent = null, $refunded = null, $refunds = [], $status = null ) { $this->id = $id; $this->amount = $amount; $this->created = $created; $this->payment_method_details = $payment_method_details; $this->payment_method = $payment_method; $this->amount_captured = $amount_captured; $this->amount_refunded = $amount_refunded; $this->application_fee_amount = $application_fee_amount; $this->balance_transaction = $balance_transaction; $this->billing_details = $billing_details; $this->currency = $currency; $this->dispute = $dispute; $this->disputed = $disputed; $this->order = $order; $this->outcome = $outcome; $this->paid = $paid; $this->paydown = $paydown; $this->payment_intent = $payment_intent; $this->refunded = $refunded; $this->refunds = $refunds; $this->status = $status; // Set default properties. $this->captured = false; } /** * Gets charge ID * * @return string */ public function get_id() { return $this->id; } /** * Gets charge amount * * @return int */ public function get_amount() { return $this->amount; } /** * Gets charge created time * * @return DateTime */ public function get_created() { return $this->created; } /** * Is the charge captured? * * @return bool */ public function is_captured() { return $this->captured; } /** * Sets charge captured flag * * @param bool $captured - Flag indicating capture status of charge. */ public function set_captured( $captured ) { $this->captured = $captured; } /** * Returns the payment method details associated with this charge * * @return array */ public function get_payment_method_details() { return $this->payment_method_details; } /** * Returns the payment method id associated with this charge * * @return string|null */ public function get_payment_method() { return $this->payment_method; } /** * Returns the amount captured associated with this charge * * @return int|null */ public function get_amount_captured() { return $this->amount_captured; } /** * Returns the amount refunded associated with this charge * * @return int|null */ public function get_amount_refunded() { return $this->amount_refunded; } /** * Returns the application fee amount associated with this charge * * @return int|null */ public function get_application_fee_amount() { return $this->application_fee_amount; } /** * Returns the balance transaction associated with this charge * * @return array */ public function get_balance_transaction() { return $this->balance_transaction; } /** * Returns the billing details associated with this charge * * @return array */ public function get_billing_details() { return $this->billing_details; } /** * Returns the currency associated with this charge * * @return string|null */ public function get_currency() { return $this->currency; } /** * Returns the dispute object associated with this charge * * @return array */ public function get_dispute() { return $this->dispute; } /** * Returns the disputed flag associated with this charge * * @return bool|null */ public function get_disputed() { return $this->disputed; } /** * Returns the order object associated with this charge * * @return array */ public function get_order() { return $this->order; } /** * Returns the outcome object associated with this charge * * @return array */ public function get_outcome() { return $this->outcome; } /** * Returns the paid flag associated with this charge * * @return bool|null */ public function get_paid() { return $this->paid; } /** * Returns the paydown object associated with this charge * * @return array */ public function get_paydown() { return $this->paydown; } /** * Returns the payment intent id associated with this charge * * @return string|null */ public function get_payment_intent() { return $this->payment_intent; } /** * Returns the refunded flag associated with this charge * * @return bool|null */ public function get_refunded() { return $this->refunded; } /** * Returns the refund object associated with this charge * * @return array */ public function get_refunds() { return $this->refunds; } /** * Returns the status associated with this charge * * @return string|null */ public function get_status() { return $this->status; } /** * Defines which data will be serialized to JSON */ public function jsonSerialize(): array { return [ 'id' => $this->get_id(), 'amount' => $this->get_amount(), 'created' => $this->get_created()->getTimestamp(), 'payment_method_details' => $this->get_payment_method_details(), 'payment_method' => $this->get_payment_method(), 'amount_captured' => $this->get_amount_captured(), 'amount_refunded' => $this->get_amount_refunded(), 'application_fee_amount' => $this->get_application_fee_amount(), 'balance_transaction' => $this->get_balance_transaction(), 'billing_details' => $this->get_billing_details(), 'currency' => $this->get_currency(), 'dispute' => $this->get_dispute(), 'disputed' => $this->get_disputed(), 'order' => $this->get_order(), 'outcome' => $this->get_outcome(), 'paid' => $this->get_paid(), 'paydown' => $this->get_paydown(), 'payment_intent' => $this->get_payment_intent(), 'captured' => $this->is_captured(), 'refunded' => $this->get_refunded(), 'refunds' => $this->get_refunds(), 'status' => $this->get_status(), ]; } } includes/wc-payment-api/class-wc-payments-http.php 0000777 00000016070 15251156605 0016266 0 ustar 00 <?php /** * WC_Payments_Http class. * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\Connection_Exception; use WCPay\Logger; /** * A wrapper around Jetpack HTTP request library. Necessary to increase * the testability of WC_Payments_API_Client, and allow dependency * injection. */ class WC_Payments_Http implements WC_Payments_Http_Interface { /** * Jetpack connection handler. * * @var Automattic\Jetpack\Connection\Manager */ private $connection_manager; /** * WC_Payments_Http constructor. * * @param Automattic\Jetpack\Connection\Manager $connection_manager - Jetpack connection handler. */ public function __construct( $connection_manager ) { $this->connection_manager = $connection_manager; } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_filter( 'allowed_redirect_hosts', [ $this, 'allowed_redirect_hosts' ] ); } /** * Sends a remote request through Jetpack. * * @param array $args - The arguments to passed to Jetpack. * @param string $body - The body passed on to the HTTP request. * @param bool $is_site_specific - If true, the site ID will be included in the request url. Defaults to true. * @param bool $use_user_token - If true, the request will be signed with the Jetpack connection owner user token rather than blog token. Defaults to false. * * @return array HTTP response on success. * @throws API_Exception - If not connected or request failed. */ public function remote_request( $args, $body = null, $is_site_specific = true, $use_user_token = false ) { // Make sure we're not sending requests if Jetpack is not connected. if ( ! $this->is_connected() ) { Logger::error( 'HTTP_REQUEST_ERROR Site is not connected to WordPress.com' ); throw new API_Exception( __( 'Site is not connected to WordPress.com', 'woocommerce-payments' ), 'wcpay_wpcom_not_connected', 409 // HTTP Conflict status code. ); } $args['blog_id'] = $this->get_blog_id(); if ( $use_user_token ) { $args['user_id'] = $this->connection_manager->get_connection_owner_id(); } if ( $is_site_specific ) { // We expect `url` to include a `%s` placeholder which will allow us inject the blog id. $url = explode( '?', $args['url'], 2 ); $url[0] = sprintf( $url[0], $args['blog_id'] ); $args['url'] = implode( '?', $url ); } return self::make_request( $args, $body ); } /** * Queries the WordPress.com REST API with a user token. * * @param string $path REST API path. * @param string $version REST API version. Default is `2`. * @param array $args Arguments to {@see WP_Http}. Default is `array()`. * @param string|array $body Body passed to {@see WP_Http}. Default is `null`. * @param string $base_api_path REST API root. Default is `wpcom`. * * @return array|\WP_Error $response Response data, else WP_Error on failure. */ public function wpcom_json_api_request_as_user( $path, $version = '2', $args = [], $body = null, $base_api_path = 'wpcom' ) { return Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user( $path, $version, $args, $body, $base_api_path ); } /** * Makes a request through Jetpack. * * @param array $args - The arguments passed to Jetpack. * @param string $body - The body passed to the HTTP request. * * @return array HTTP response on success. * @throws Connection_Exception - If request returns WP_Error. */ private static function make_request( $args, $body ) { $response = Automattic\Jetpack\Connection\Client::remote_request( $args, $body ); if ( is_wp_error( $response ) || ! is_array( $response ) ) { Logger::error( 'HTTP_REQUEST_ERROR ' . var_export( $response, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export $message = sprintf( // translators: %1: original error message. __( 'Http request failed. Reason: %1$s', 'woocommerce-payments' ), $response->get_error_message() ); throw new Connection_Exception( $message, 'wcpay_http_request_failed', 500 ); } return $response; } /** * Checks if Jetpack is connected. * * Checks if connection is authenticated in the same way as Jetpack_Client or Jetpack Connection Client does. * * @return bool true if Jetpack connection has access token. */ public function is_connected() { return $this->connection_manager->is_connected() && $this->connection_manager->has_connected_owner(); } /** * Checks if the current user is connected to WordPress.com. * * @return bool true if the current user is connected. */ public function is_user_connected() { return $this->connection_manager->is_user_connected(); } /** * Get the wpcom user data of the current connected user. * * @return bool|array An array with the WPCOM user data on success, false otherwise. */ public function get_connected_user_data() { return $this->connection_manager->get_connected_user_data(); } /** * Checks if the site has an admin who is also a connection owner. * * @return bool True if Jetpack connection has an owner. */ public function has_connection_owner() { return ! empty( $this->connection_manager->get_connection_owner_id() ); } /** * Gets the current WP.com blog ID. * * @return integer Current WPCOM blog ID. */ public function get_blog_id() { return Jetpack_Options::get_option( 'id' ); } /** * Starts the Jetpack connection process. Note that running this function will immediately redirect * to the Jetpack flow, so any PHP code after it will never be executed. * * @param string $redirect - URL to redirect to after the connection process is over. * * @throws API_Exception - Exception thrown on failure. */ public function start_connection( $redirect ) { // Register the site to wp.com. if ( ! $this->connection_manager->is_connected() ) { $result = $this->connection_manager->try_registration(); if ( is_wp_error( $result ) ) { throw new API_Exception( $result->get_error_message(), 'wcpay_jetpack_register_site_failed', 500 ); } } // Redirect the user to the Jetpack user connection flow. add_filter( 'jetpack_use_iframe_authorization_flow', '__return_false' ); // Same logic as in WC-Admin. $calypso_env = defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, [ 'development', 'wpcalypso', 'horizon', 'stage' ], true ) ? WOOCOMMERCE_CALYPSO_ENVIRONMENT : 'production'; wp_safe_redirect( add_query_arg( [ 'from' => 'woocommerce-core-profiler', 'plugin_name' => 'woocommerce-payments', 'calypso_env' => $calypso_env, ], $this->connection_manager->get_authorization_url( null, $redirect ) ) ); exit; } /** * Filter function to add WP.com to the list of allowed redirect hosts * * @param array $hosts - array of allowed hosts. * * @return array allowed hosts */ public function allowed_redirect_hosts( $hosts ) { $hosts[] = 'jetpack.wordpress.com'; return $hosts; } } includes/class-wc-payments-settings-service.php 0000777 00000001402 15251156605 0015743 0 ustar 00 <?php /** * WooCommerce Payments WC_Payments_Settings_Service Class * * @package WooCommerce\Payments */ use WCPay\MultiCurrency\Interfaces\MultiCurrencySettingsInterface; defined( 'ABSPATH' ) || exit; /** * WC_Payments_Settings_Service. */ class WC_Payments_Settings_Service implements MultiCurrencySettingsInterface { /** * Checks if dev mode is enabled. * * @return bool */ public function is_dev_mode(): bool { return WC_Payments::mode()->is_dev(); } /** * Gets the plugin file path. * * @return string */ public function get_plugin_file_path(): string { return WCPAY_PLUGIN_FILE; } /** * Gets the plugin version. * * @return string */ public function get_plugin_version(): string { return WCPAY_VERSION_NUMBER; } } includes/class-wc-payments-fraud-service.php 0000777 00000027600 15251156605 0015214 0 ustar 00 <?php /** * WC_Payments_Fraud_Service class * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; use WCPay\Database_Cache; use WCPay\Exceptions\API_Exception; use WCPay\Logger; /** * Class which includes all the fraud-specific logic. */ class WC_Payments_Fraud_Service { /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * WC_Payments_Account instance to get information about the account * * @var WC_Payments_Account */ private $account; /** * WC_Payments_Customer instance for working with customer information * * @var WC_Payments_Customer_Service */ private $customer_service; /** * WC_Payments_Session_Service instance for working with session information * * @var WC_Payments_Session_Service */ private $session_service; /** * Cache util for managing the database cached data. * * @var Database_Cache */ private $database_cache; /** * Constructor for WC_Payments_Fraud_Service. * * @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client. * @param WC_Payments_Customer_Service $customer_service - Customer class instance. * @param WC_Payments_Account $account - Account class instance. * @param WC_Payments_Session_Service $session_service - Session Service class instance. * @param Database_Cache $database_cache - Database cache instance. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Customer_Service $customer_service, WC_Payments_Account $account, WC_Payments_Session_Service $session_service, Database_Cache $database_cache ) { $this->payments_api_client = $payments_api_client; $this->customer_service = $customer_service; $this->account = $account; $this->session_service = $session_service; $this->database_cache = $database_cache; } /** * Initializes this class's hooks. * * @return void */ public function init_hooks() { add_action( 'init', [ $this, 'link_session_if_user_just_logged_in' ] ); add_action( 'admin_print_footer_scripts', [ $this, 'add_sift_js_tracker_in_admin' ] ); } /** * Gets the various anti-fraud services that must be included on every WCPay-related page. * * @return array Assoc array. Each key is the slug of a fraud service that must be incorporated to every page. * The value is a service-specific config for it. */ public function get_fraud_services_config(): array { $raw_config = null; // First, try to get the config from the account data. // This config takes precedence since it can be merchant-specific. // We expect this entry to contain everything needed for the fraud services to work. $account = $this->account->get_cached_account_data(); if ( ! empty( $account ) && isset( $account['fraud_services'] ) ) { $raw_config = $account['fraud_services']; } // If the fraud services config is not available in the account data, try to get it from the server. // This is a public, merchant-agnostic config. // We expect the server to provide everything needed for the fraud services to work. // If we've been given an empty array, we respect that; so no empty checks. if ( is_null( $raw_config ) ) { $raw_config = $this->get_cached_fraud_services(); } if ( is_null( $raw_config ) ) { // This was the default before adding new anti-fraud providers, preserve backwards-compatibility. $raw_config = [ 'stripe' => [] ]; } $services_config = []; foreach ( $raw_config as $service_id => $config ) { if ( ! is_array( $config ) ) { $config = []; } // Apply our internal logic before allowing others to have a say through filters. $config = $this->prepare_fraud_config( $config, $service_id ); $services_config[ $service_id ] = apply_filters( 'wcpay_prepare_fraud_config', $config, $service_id ); } return $services_config; } /** * Check if the current user has just logged in, * and sends that information to the server to link the current browser session with the user. * * Called after the WooCommerce session has been initialized. * * @return void * * @throws Exception In case the main gateway class has not been initialized yet. * This means that the method is called before the `init` hook. */ public function link_session_if_user_just_logged_in() { $wpcom_blog_id = $this->payments_api_client->get_blog_id(); if ( ! $wpcom_blog_id ) { // Don't do anything if Jetpack hasn't been connected yet. return; } if ( ! $this->session_service->user_just_logged_in() ) { return; } $fraud_config = $this->get_fraud_services_config(); if ( ! isset( $fraud_config['sift'] ) ) { // If Sift isn't enabled, we don't need to link the session. return; } // The session changed during the current page load, for example if the user just logged in. // In this case, send the old session's customer ID alongside the new user_id so SIFT can link them. $customer_id = $this->customer_service->get_customer_id_by_user_id( get_current_user_id() ); if ( ! isset( $customer_id ) ) { return; } try { $this->session_service->link_current_session_to_customer( $customer_id ); } catch ( API_Exception $e ) { Logger::log( '[Tracking] Error when linking session with user: ' . $e->getMessage() ); } } /** * Adds the Sift JS page tracker in the WP admin area, if needed. * * @return void * * @throws Exception In case the main gateway class has not been initialized yet. * This means that the method is called before the `init` hook. */ public function add_sift_js_tracker_in_admin() { // If the current page is a WooPayments dashboard page bail as there's separate logic // that will include the Sift JS tracker on its own. if ( isset( $_GET['path'] ) && strpos( $_GET['path'], '/payments/' ) === 0 ) { // phpcs:ignore WordPress.Security return; } // Bail if this is not a WooCommerce admin page. if ( is_callable( '\Automattic\WooCommerce\Admin\PageController::is_admin_or_embed_page' ) && ! \Automattic\WooCommerce\Admin\PageController::is_admin_or_embed_page() ) { return; } // Bail if Sift is not enabled (either globally or for the current account). $fraud_services_config = $this->get_fraud_services_config(); if ( ! isset( $fraud_services_config['sift'] ) ) { return; } ?> <script type="text/javascript"> var src = 'https://cdn.sift.com/s.js'; var _sift = ( window._sift = window._sift || [] ); _sift.push( [ '_setAccount', '<?php echo esc_attr( $fraud_services_config['sift']['beacon_key'] ); ?>' ] ); _sift.push( [ '_setUserId', '<?php echo esc_attr( $fraud_services_config['sift']['user_id'] ); ?>' ] ); _sift.push( [ '_setSessionId', '<?php echo esc_attr( $fraud_services_config['sift']['session_id'] ); ?>' ] ); _sift.push( [ '_trackPageview' ] ); if ( ! document.querySelector( '[src="' + src + '"]' ) ) { var script = document.createElement( 'script' ); script.src = src; script.async = true; document.body.appendChild( script ); } </script> <?php } /** * Checks if the cached fraud services config can be used. * * @param bool|string|array $fraud_services_config The cached config. * * @return bool True if the cached fraud services config is valid. */ public function is_valid_cached_fraud_services( $fraud_services_config ): bool { // null/false means no config has been cached. if ( null === $fraud_services_config || false === $fraud_services_config ) { return false; } // Non-array values are not expected. if ( ! is_array( $fraud_services_config ) ) { return false; } return true; } /** * Gets and caches the public fraud services config. * * @return array|null Fraud services config or null if failed to retrieve fraud services config. */ private function get_cached_fraud_services(): ?array { return $this->database_cache->get_or_add( Database_Cache::FRAUD_SERVICES_KEY, function () { $fraud_services = $this->fetch_public_fraud_services_config(); if ( ! $this->is_valid_cached_fraud_services( $fraud_services ) ) { return false; } // Sanitize the config just to be safe by applying a sweeping `sanitize_text_field` on all the data. // This is OK to do since we are not accepting data entries with HTML. return WC_Payments_Utils::array_map_recursive( $fraud_services, function ( $value ) { // Only apply `sanitize_text_field()` to string values since this function will cast to string. if ( is_string( $value ) ) { return sanitize_text_field( $value ); } return $value; } ); }, [ $this, 'is_valid_cached_fraud_services' ] ); } /** * Prepares the fraud config for a service. * * @param array $config Existing config data for the given anti-fraud service. * @param string $service_id Identifier of the anti-fraud service provider. * * @return array|NULL Array with all the required data to initialize the anti-fraud script, or NULL if the service shouldn't be used. */ private function prepare_fraud_config( array $config, string $service_id ): ?array { switch ( $service_id ) { case 'sift': return $this->prepare_sift_config( $config ); } return $config; } /** * Adds site-specific config needed to initialize the SIFT anti-fraud JS. * * @param array $config Associative array with the SIFT-related configuration returned from the server. * * @return array|null Assoc array, ready for the client to consume, or NULL if the client shouldn't enqueue this script. */ private function prepare_sift_config( array $config ): ?array { $test_mode = false; try { $test_mode = WC_Payments::mode()->is_test(); } catch ( Exception $e ) { Logger::log( sprintf( 'WooPayments JS settings: Could not determine if WCPay should be in test mode! Message: %s', $e->getMessage() ), 'warning' ); } // The server returns both production and sandbox beacon keys. Use the sandbox one if test mode is enabled. if ( $test_mode && isset( $config['sandbox_beacon_key'] ) ) { $config['beacon_key'] = $config['sandbox_beacon_key']; } unset( $config['sandbox_beacon_key'] ); $config['user_id'] = $this->get_sift_user_id(); $config['session_id'] = $this->session_service->get_sift_session_id(); return $config; } /** * Get the Sift user ID to use for the current request. * * @return string The Sift user ID to use for the current request. Empty string if we couldn't determine one. */ private function get_sift_user_id(): string { $user_id = ''; if ( ! is_user_logged_in() ) { return $user_id; } if ( is_admin() ) { // In the WP admin we deal with merchant accounts, not customers. $user_id = $this->account->get_stripe_account_id() ?? ''; } else { $customer_id = $this->customer_service->get_customer_id_by_user_id( get_current_user_id() ); if ( isset( $customer_id ) ) { $user_id = $customer_id; } } return $user_id; } /** * Fetches public fraud services config from the server. * * @return array|null Fraud services config or null. */ private function fetch_public_fraud_services_config(): ?array { // Build the endpoint URL. $url = WC_Payments_API_Client::ENDPOINT_BASE . '/' . WC_Payments_API_Client::ENDPOINT_REST_BASE . '/' . WC_Payments_API_Client::FRAUD_SERVICES_API; $response = wp_remote_get( $url, [ 'user-agent' => 'WCPay/' . WCPAY_VERSION_NUMBER . '; ' . get_bloginfo( 'url' ), ] ); // Return early if there is an error. if ( is_wp_error( $response ) ) { return null; } $config = null; if ( 200 === wp_remote_retrieve_response_code( $response ) ) { // Decode the results, falling back to an empty array. $config = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $config ) || ! is_array( $config ) ) { $config = null; } } return $config; } } includes/class-wc-payments-pm-promotions-service.php 0000777 00000076373 15251156605 0016751 0 ustar 00 <?php /** * Class WC_Payments_PM_Promotions_Service * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Constants\Track_Events; use WCPay\Core\Server\Request; use WCPay\Logger; /** * Class handling WooPayments payment method promotions related business logic. */ class WC_Payments_PM_Promotions_Service { /** * Transient key for caching promotions. * * @var string */ const PROMOTIONS_CACHE_KEY = 'wcpay_pm_promotions'; /** * Option key for promotion dismissals. * Stores array of [id => timestamp]. * * @var string */ const PROMOTION_DISMISSALS_OPTION = '_wcpay_pm_promotion_dismissals'; /** * The memoized raw promotions to avoid fetching multiple times during a request. * * @var array|null */ private $promotions_memo = null; /** * The memoized visible promotions (filtered and normalized) for the current request. * False means not yet computed, null means computed with no results, array means has results. * * @var array|null|false */ private $visible_promotions_memo = false; /** * WC_Payment_Gateway_WCPay instance. * * @var WC_Payment_Gateway_WCPay|null */ private $gateway; /** * WC_Payments_Account instance. * * @var WC_Payments_Account|null */ private $account; /** * Class constructor. * * @param WC_Payment_Gateway_WCPay|null $gateway Optional gateway instance. * @param WC_Payments_Account|null $account Optional account instance. */ public function __construct( ?WC_Payment_Gateway_WCPay $gateway = null, ?WC_Payments_Account $account = null ) { $this->gateway = $gateway; $this->account = $account; } /** * Initialise class hooks. * * @return void */ public function init_hooks() { // Hooks can be added here if needed in the future. } /** * Clear the promotions cache. * * @return void */ public function clear_cache(): void { delete_transient( self::PROMOTIONS_CACHE_KEY ); $this->reset_memo(); } /** * Reset the memoized promotions. * * This is useful for testing purposes. * * @return void */ public function reset_memo(): void { $this->promotions_memo = null; $this->visible_promotions_memo = false; } /** * Get promotions that should be visible to the user. * * @return array|null The promotions or null if there is no eligible promotion. */ public function get_visible_promotions(): ?array { // Promotions are only visible to users who can manage WooCommerce (aka act on the promotions). if ( ! current_user_can( 'manage_woocommerce' ) ) { return null; } // Return memoized result if available (false means not yet computed). if ( false !== $this->visible_promotions_memo ) { return $this->visible_promotions_memo; } $promotions = $this->get_promotions(); // Validate each promotion's structure. $promotions = array_filter( $promotions, function ( $promotion ) { return $this->validate_promotion( $promotion ); } ); // Filter promotions by dismissal status, PM validity, enabled status, and promo_id uniqueness. $promotions = $this->filter_promotions( $promotions ); // Normalize the promotions (apply fallbacks, derive fields). $promotions = $this->normalize_promotions( $promotions ); // Return early if there are no promotions left. if ( empty( $promotions ) ) { $this->visible_promotions_memo = null; return null; } $this->visible_promotions_memo = array_values( $promotions ); return $this->visible_promotions_memo; } /** * Fetches and caches eligible promotions from the WooPayments API. * * @return array List of eligible promotions. */ private function get_promotions(): array { // Check memoized data first. if ( null !== $this->promotions_memo ) { return $this->promotions_memo; } // Try to use the cached data. $cache = get_transient( self::PROMOTIONS_CACHE_KEY ); // If the cached data is not expired, and it's a WP_Error, // it means there was an API error previously, and we should not retry just yet. if ( is_wp_error( $cache ) ) { // Initialize the in-memory cache and return it. $this->promotions_memo = []; return $this->promotions_memo; } // Gather the store context data. $store_context = [ // All the PM promotions dismissals. 'dismissals' => $this->get_promotion_dismissals(), // Store locale, e.g. `en_US`. 'locale' => get_locale(), ]; // Fingerprint the store context through a hash of certain entries. $store_context_hash = $this->generate_context_hash( $store_context ); // Use the transient cached data if it exists, it is not expired, // and the store context hasn't changed since we last requested from the WooPayments API (based on context hash). if ( false !== $cache && ! empty( $cache['context_hash'] ) && is_string( $cache['context_hash'] ) && hash_equals( $store_context_hash, $cache['context_hash'] ) ) { // We have a store context hash, and it matches with the current context one. // We can use the cached data. $this->promotions_memo = $cache['promotions'] ?? []; return $this->promotions_memo; } // By this point, we have an expired transient or the store context has changed. // Query for promotions by calling the WooPayments API. $wcpay_request = Request\Get_PM_Promotions::create(); $wcpay_request->set_store_context_params( $store_context ); $response = $wcpay_request->handle_rest_request(); // Return early if there is an error, waiting 6 hours before the next attempt. if ( is_wp_error( $response ) ) { // Store a trimmed down, lightweight error. /** * Type hint for static analysis. * * @var WP_Error $response */ $error = new \WP_Error( $response->get_error_code(), $response->get_error_message(), wp_remote_retrieve_response_code( $response ) ); // Store the error in the transient so we know this is due to an API error. set_transient( self::PROMOTIONS_CACHE_KEY, $error, HOUR_IN_SECONDS * 6 ); // Initialize the in-memory cache and return it. $this->promotions_memo = []; return $this->promotions_memo; } $cache_for = wp_remote_retrieve_header( $response, 'cache-for' ); // Initialize the in-memory cache. $this->promotions_memo = []; if ( 200 === wp_remote_retrieve_response_code( $response ) ) { // Decode the results, falling back to an empty array. $results = json_decode( wp_remote_retrieve_body( $response ), true ) ?? []; $this->promotions_memo = $results; } // Skip transient cache if `cache-for` header equals zero. if ( '0' === $cache_for ) { // If we have a transient cache that is not expired, delete it so there are no leftovers. if ( false !== $cache ) { delete_transient( self::PROMOTIONS_CACHE_KEY ); } return $this->promotions_memo; } // Store promotions in the transient cache (together with the context hash) for the given number of seconds // or 1 day in seconds. Also attach a timestamp to the transient data so we know when we last fetched. set_transient( self::PROMOTIONS_CACHE_KEY, [ 'promotions' => $this->promotions_memo, 'context_hash' => $store_context_hash, 'timestamp' => time(), ], ! empty( $cache_for ) ? (int) $cache_for : DAY_IN_SECONDS ); return $this->promotions_memo; } /** * Activate a promotion. * * This will: * 1. Send a request to the server to apply the promotion discount. * 2. Enable the payment method for checkout. * Activating a promotion implies acceptance of terms and conditions for the promotion. * * @param string $id The promotion unique identifier (e.g., 'klarna-2026-promo__spotlight'). * * @return bool True on success, false on failure. */ public function activate_promotion( string $id ): bool { // Find the promotion to get the payment method. $promotion = $this->find_promotion_by_id( $id ); if ( null === $promotion ) { return false; } $payment_method_id = $promotion['payment_method'] ?? ''; if ( empty( $payment_method_id ) ) { return false; } // Send request to server to apply the promotion discount. // The server should also handle capability requesting if it is not already requested. // This way we can keep things in sync and avoid applying discounts without having the capability requested. $wcpay_request = Request\Activate_PM_Promotion::create( $id ); $wcpay_request->assign_hook( 'wcpay_activate_pm_promotion_request' ); $response = $wcpay_request->handle_rest_request(); if ( is_wp_error( $response ) ) { $error_message = sprintf( 'Server activation request failed [%s]: %s', $response->get_error_code(), $response->get_error_message() ); return $this->handle_promotion_activation_failure( $payment_method_id, $promotion, $error_message ); } // Mark the promotion as dismissed so it won't be shown again. // Do it before the payment method gateway enabling in case that fails. $this->mark_promotion_dismissed( $id ); // Enable the payment method for checkout. if ( ! $this->enable_payment_method_gateway( $payment_method_id, $promotion ) ) { return false; } // Clear the promotions cache to ensure fresh data on next fetch. $this->clear_cache(); // Clear the account cache. if ( null !== $this->account ) { $this->account->clear_cache(); } // Track successful activation. $this->tracks_event( Track_Events::PAYMENT_METHOD_PROMOTION_ACTIVATED, [ 'payment_method_id' => $payment_method_id, 'promo_id' => $promotion['promo_id'] ?? null, 'promo_instance_id' => $id, ] ); return true; } /** * Find a promotion by its ID. * * @param string $id The promotion ID (e.g., 'klarna-2026-promo__spotlight'). * * @return array|null The promotion data or null if not found. */ private function find_promotion_by_id( string $id ): ?array { $promotions = $this->get_visible_promotions(); if ( null === $promotions ) { return null; } foreach ( $promotions as $promotion ) { if ( isset( $promotion['id'] ) && $promotion['id'] === $id ) { return $promotion; } } return null; } /** * Find a promotion for a payment method. * * We will return the first promotion found for the payment method. * * @param string $payment_method_id The payment method ID (e.g., 'klarna'). * * @return array|null The promotion data or null if not found. */ private function find_promotion_by_payment_method( string $payment_method_id ): ?array { $promotions = $this->get_visible_promotions(); if ( null === $promotions ) { return null; } foreach ( $promotions as $promotion ) { if ( isset( $promotion['payment_method'] ) && $promotion['payment_method'] === $payment_method_id ) { return $promotion; } } return null; } /** * Enable a payment method gateway. * * @param string $payment_method_id The payment method ID (e.g., 'klarna'). * @param array $promotion The promotion data associated with the payment method. * * @return bool True on success, false on failure. */ private function enable_payment_method_gateway( string $payment_method_id, array $promotion ): bool { $gateway = WC_Payments::get_payment_gateway_by_id( $payment_method_id ); if ( ! $gateway ) { $this->log_gateway_error( $payment_method_id, 'payment gateway instance not available' ); return false; } // Attempt to enable the gateway with exception handling. try { $gateway->enable(); } catch ( \Exception $e ) { $this->log_gateway_error( $payment_method_id, $e->getMessage() ); return false; } // Verify the gateway was actually enabled. if ( 'yes' !== $gateway->get_option( 'enabled' ) ) { $this->log_gateway_error( $payment_method_id, 'gateway enable() did not persist enabled state' ); return false; } $pm_to_capability_key_map = $gateway->get_payment_method_capability_key_map(); $this->tracks_event( Track_Events::PAYMENT_METHOD_ENABLED, [ 'payment_method_id' => $payment_method_id, 'capability_id' => $pm_to_capability_key_map[ $payment_method_id ] ?? null, 'promo_id' => $promotion['promo_id'] ?? null, ] ); // Synchronize enabled payment method IDs across all gateways. $this->sync_enabled_payment_method_across_gateways( $payment_method_id ); return true; } /** * Log a gateway error. * * @param string $payment_method_id The payment method ID. * @param string $error_message The error message. */ private function log_gateway_error( string $payment_method_id, string $error_message ): void { /* translators: 1: Payment method ID, 2: Error message */ WC_Payments_Utils::log_to_wc( sprintf( 'Failed to enable payment method %1$s: %2$s', $payment_method_id, $error_message ), 'warning' ); } /** * Synchronize enabled payment method ID across all gateways. * * @param string $payment_method_id The payment method ID to sync. */ private function sync_enabled_payment_method_across_gateways( string $payment_method_id ): void { $gateway_map = WC_Payments::get_payment_gateway_map(); if ( empty( $gateway_map ) ) { return; } foreach ( $gateway_map as $payment_gateway ) { $enabled_pm_ids = $payment_gateway->get_upe_enabled_payment_method_ids(); // Skip if already present or not a valid array. if ( ! is_array( $enabled_pm_ids ) || in_array( $payment_method_id, $enabled_pm_ids, true ) ) { continue; } $enabled_pm_ids[] = $payment_method_id; $result = $payment_gateway->update_option( 'upe_enabled_payment_method_ids', $enabled_pm_ids ); if ( false === $result ) { WC_Payments_Utils::log_to_wc( sprintf( 'Failed to sync payment method %s to gateway %s', $payment_method_id, get_class( $payment_gateway ) ), 'warning' ); } } } /** * Activate any visible promotions for a payment method being enabled via settings. * * This method should be called BEFORE the payment method is enabled for checkout, * as visible promotions are filtered out for already-enabled payment methods. * * Handles its own exception catching, logging, and tracking internally. * * @param string $payment_method_id The payment method ID (e.g., 'klarna'). * @param bool $should_enable Whether to enable the payment method for checkout. * * @return bool True if a promotion was activated, false otherwise. */ public function maybe_activate_promotion_for_payment_method( string $payment_method_id, bool $should_enable = false ): bool { $promotion = $this->find_promotion_by_payment_method( $payment_method_id ); if ( null === $promotion ) { return false; } // Send request to server to apply the promotion discount. // The server should also handle capability requesting if it is not already requested. $wcpay_request = Request\Activate_PM_Promotion::create( $promotion['id'] ); $wcpay_request->assign_hook( 'wcpay_activate_pm_promotion_request' ); $response = $wcpay_request->handle_rest_request(); if ( is_wp_error( $response ) ) { $error_message = sprintf( 'Server activation request failed [%s]: %s', $response->get_error_code(), $response->get_error_message() ); return $this->handle_promotion_activation_failure( $payment_method_id, $promotion, $error_message ); } // Enable the payment method for checkout if requested. if ( $should_enable && ! $this->enable_payment_method_gateway( $payment_method_id, $promotion ) ) { return $this->handle_promotion_activation_failure( $payment_method_id, $promotion, 'Failed to enable payment method gateway' ); } // Clear the promotions cache to ensure fresh data on next fetch. $this->clear_cache(); // Clear the account cache. if ( null !== $this->account ) { $this->account->clear_cache(); } // Track successful activation. $this->tracks_event( Track_Events::PAYMENT_METHOD_PROMOTION_ACTIVATED, [ 'payment_method_id' => $payment_method_id, 'promo_id' => $promotion['promo_id'] ?? null, // The `unique_promo_id` is excluded intentionally as it's not a reliable without a specific promo type. ] ); return true; } /** * Handle promotion activation failure by logging and tracking. * * @param string $payment_method_id The payment method ID. * @param array $promotion The promotion data. * @param string $error_message The error message. * * @return bool Always returns false. */ private function handle_promotion_activation_failure( string $payment_method_id, array $promotion, string $error_message ): bool { /* translators: 1: Payment method ID, 2: Error message */ WC_Payments_Utils::log_to_wc( sprintf( 'Failed to activate promotion for payment method %1$s: %2$s', $payment_method_id, $error_message ) ); // Track the failure. $this->tracks_event( Track_Events::PAYMENT_METHOD_PROMOTION_ACTIVATION_FAILED, [ 'payment_method_id' => $payment_method_id, 'promo_id' => $promotion['promo_id'] ?? null, ] ); return false; } /** * Dismiss a promotion. * * @param string $id The promotion unique identifier (e.g., 'klarna-2026-promo__spotlight'). * * @return bool True if dismissed, false if already dismissed or error. */ public function dismiss_promotion( string $id ): bool { // Cannot dismiss a non-existing promotion. $promotion = $this->find_promotion_by_id( $id ); if ( null === $promotion ) { return false; } if ( ! $this->mark_promotion_dismissed( $id ) ) { return false; } // Track dismissal event. $this->tracks_event( Track_Events::PAYMENT_METHOD_PROMOTION_DISMISSED, [ 'payment_method_id' => $promotion['payment_method'] ?? null, 'promo_id' => $promotion['promo_id'] ?? null, 'promo_instance_id' => $id, ] ); // Reset memo to ensure fresh data on next access. // The context hash change will also invalidate the transient cache. $this->reset_memo(); return true; } /** * Mark a promotion as dismissed in local state. * * @param string $id The promotion unique identifier (e.g., 'klarna-2026-promo__spotlight'). * * @return bool True if dismissed, false if already dismissed. */ private function mark_promotion_dismissed( string $id ): bool { // Don't dismiss if already dismissed. if ( $this->is_promotion_dismissed( $id ) ) { return false; } $dismissals = $this->get_promotion_dismissals(); $dismissals[ $id ] = time(); return update_option( self::PROMOTION_DISMISSALS_OPTION, $dismissals, false ); } /** * Get all promotion dismissals. * * @return array Associative array of [id => timestamp]. */ public function get_promotion_dismissals(): array { return get_option( self::PROMOTION_DISMISSALS_OPTION, [] ); } /** * Check if a promotion has been dismissed. * * Being dismissed means having an entry in the dismissals option with a timestamp into the past. * * @param string $id The promotion unique identifier. * * @return bool True if dismissed, false otherwise. */ public function is_promotion_dismissed( string $id ): bool { $dismissals = $this->get_promotion_dismissals(); return isset( $dismissals[ $id ] ) && is_int( $dismissals[ $id ] ) && $dismissals[ $id ] > 0 && $dismissals[ $id ] <= time(); } /** * Check whether the promotion data is valid. * Validates required fields based on promotion type. * * @param mixed $promotion_data The promotion data. * * @return bool Whether the promotion data is valid. */ private function validate_promotion( $promotion_data ): bool { if ( ! is_array( $promotion_data ) || empty( $promotion_data ) ) { return false; } // Required fields for all promotions. $required_fields = [ 'id', 'promo_id', 'payment_method', 'type', 'title', 'description', 'tc_url' ]; foreach ( $required_fields as $field ) { if ( ! isset( $promotion_data[ $field ] ) || ! is_string( $promotion_data[ $field ] ) ) { return false; } } // Validate type is supported. $valid_types = [ 'spotlight', 'badge' ]; if ( ! in_array( $promotion_data['type'], $valid_types, true ) ) { return false; } return true; } /** * Generate a hash from the store context data. * * @param array $context The store context data. * * @return string The context hash. */ private function generate_context_hash( array $context ): string { // Include only certain entries in the context hash. // We need only discrete, user-interaction dependent data. // Do not include information that changes automatically (e.g., time since activation, etc.). return md5( wp_json_encode( [ 'dismissals' => $context['dismissals'] ?? [], 'locale' => $context['locale'] ?? '', ] ) ); } /** * Get list of valid payment method IDs from the gateway. * * @return array List of valid payment method IDs. */ private function get_valid_payment_method_ids(): array { if ( null === $this->gateway ) { $this->gateway = WC_Payments::get_gateway(); } if ( null === $this->gateway || ! is_callable( [ $this->gateway, 'get_upe_available_payment_methods' ] ) ) { return []; } $result = $this->gateway->get_upe_available_payment_methods(); return is_array( $result ) ? $result : []; } /** * Get list of enabled payment method IDs. * * @return array List of enabled payment method IDs. */ private function get_enabled_payment_method_ids(): array { if ( null === $this->gateway ) { $this->gateway = WC_Payments::get_gateway(); } if ( null === $this->gateway || ! is_callable( [ $this->gateway, 'get_upe_enabled_payment_method_ids' ] ) ) { return []; } $result = $this->gateway->get_upe_enabled_payment_method_ids(); return is_array( $result ) ? $result : []; } /** * Get the account fees. * * @return array Account fees indexed by payment method ID. */ private function get_account_fees(): array { if ( null === $this->account ) { $this->account = WC_Payments::get_account_service(); } if ( null === $this->account || ! is_callable( [ $this->account, 'get_fees' ] ) ) { return []; } $result = $this->account->get_fees(); return is_array( $result ) ? $result : []; } /** * Check if a payment method has an active discount. * * @param string $payment_method_id The payment method ID. * @param array|null $account_fees Optional. Pre-fetched account fees. If null, will be fetched. * * @return bool True if the payment method has an active discount. */ private function payment_method_has_active_discount( string $payment_method_id, ?array $account_fees = null ): bool { if ( null === $account_fees ) { $account_fees = $this->get_account_fees(); } if ( empty( $account_fees[ $payment_method_id ] ) ) { return false; } $pm_fees = $account_fees[ $payment_method_id ]; // Verify discount is a non-empty array. if ( ! isset( $pm_fees['discount'] ) || ! is_array( $pm_fees['discount'] ) || empty( $pm_fees['discount'] ) ) { return false; } // Get first discount entry regardless of array key structure. $first_discount = reset( $pm_fees['discount'] ); if ( is_array( $first_discount ) && array_key_exists( 'discount', $first_discount ) && ! empty( $first_discount['discount'] ) ) { return true; } return false; } /** * Filter promotions by dismissal status, payment method validity, enabled status, discount status, * and promo_id uniqueness per payment method. * * @param array $promotions Array of promotions. * * @return array Filtered promotions. */ private function filter_promotions( array $promotions ): array { // Pre-fetch all data needed for filtering to avoid N+1 queries. $enabled_pms = $this->get_enabled_payment_method_ids(); $valid_pms = $this->get_valid_payment_method_ids(); $account_fees = $this->get_account_fees(); $seen_promo_ids = []; // Track first promo_id per PM. $filtered = []; foreach ( $promotions as $promotion ) { $id = $promotion['id'] ?? ''; $pm_id = $promotion['payment_method'] ?? ''; $promo_id = $promotion['promo_id'] ?? ''; // Filters ordered by performance cost (cheapest first, all use pre-fetched data). // 1. Skip promotions for already enabled payment methods. if ( in_array( $pm_id, $enabled_pms, true ) ) { continue; } // 2. Skip invalid payment methods. if ( ! in_array( $pm_id, $valid_pms, true ) ) { continue; } // 3. Skip dismissed promotions (WP cached option). if ( $this->is_promotion_dismissed( $id ) ) { continue; } // 4. Skip promotions for payment methods that already have an active discount. if ( $this->payment_method_has_active_discount( $pm_id, $account_fees ) ) { continue; } // 5. Track first promo_id per PM - keep all surfaces for that promo_id. // Must be last as it has side effects (tracks seen promo_ids). if ( ! isset( $seen_promo_ids[ $pm_id ] ) ) { $seen_promo_ids[ $pm_id ] = $promo_id; } // Skip if this is a different promo_id for an already-seen PM. if ( $seen_promo_ids[ $pm_id ] !== $promo_id ) { continue; } $filtered[] = $promotion; } return $filtered; } /** * Normalize promotions by applying fallbacks and deriving fields. * * @param array $promotions Array of promotions. * * @return array Normalized promotions. */ private function normalize_promotions( array $promotions ): array { $normalized = []; foreach ( $promotions as $promotion ) { // These fields are validated as required before normalization. $pm_id = $promotion['payment_method']; $tc_url = $promotion['tc_url']; // Add derived payment_method_title if not provided. if ( empty( $promotion['payment_method_title'] ) ) { $promotion['payment_method_title'] = $this->get_payment_method_title( $pm_id ); } // Apply fallback for cta_label using the final payment_method_title. if ( empty( $promotion['cta_label'] ) ) { /* translators: %s is the payment method title, e.g., "Klarna" */ $promotion['cta_label'] = sprintf( __( 'Enable %s', 'woocommerce-payments' ), $promotion['payment_method_title'] ); } // Apply type-specific sanitization BEFORE tc_label fallback. // This ensures we check against the sanitized description (which might lose the link). $promotion = $this->sanitize_promotion( $promotion ); // Apply fallback for tc_label only if tc_url is not already in the sanitized description. // If tc_url is in the description, leaving tc_label empty signals frontend to not add a link. if ( empty( $promotion['tc_label'] ) ) { if ( strpos( $promotion['description'], $tc_url ) === false ) { $promotion['tc_label'] = __( 'See terms', 'woocommerce-payments' ); } else { // Explicitly set to empty string when skipping fallback. $promotion['tc_label'] = ''; } } $normalized[] = $promotion; } return $normalized; } /** * Sanitize a promotion's fields based on its type. * * @param array $promotion The promotion data. * * @return array Sanitized promotion. */ private function sanitize_promotion( array $promotion ): array { $type = $promotion['type'] ?? ''; // Sanitize identifier fields strictly with sanitize_key. $key_fields = [ 'id', 'promo_id', 'payment_method', 'type' ]; foreach ( $key_fields as $field ) { if ( isset( $promotion[ $field ] ) ) { $promotion[ $field ] = sanitize_key( $promotion[ $field ] ); } } // Sanitize text fields (no HTML allowed). $text_fields = [ 'payment_method_title', 'title', 'cta_label', 'tc_label', 'badge_text' ]; foreach ( $text_fields as $field ) { if ( isset( $promotion[ $field ] ) ) { $promotion[ $field ] = sanitize_text_field( $promotion[ $field ] ); } } // Normalize badge_type: ensure it's a valid type, defaulting to 'success'. $valid_badge_types = [ 'primary', 'success', 'light', 'warning', 'alert' ]; $promotion['badge_type'] = isset( $promotion['badge_type'] ) && in_array( $promotion['badge_type'], $valid_badge_types, true ) ? $promotion['badge_type'] : 'success'; // Sanitize URL fields. if ( isset( $promotion['tc_url'] ) ) { $promotion['tc_url'] = esc_url_raw( $promotion['tc_url'] ); } if ( isset( $promotion['image'] ) ) { $promotion['image'] = esc_url_raw( $promotion['image'] ); } // Sanitize description based on type. if ( isset( $promotion['description'] ) ) { $promotion['description'] = $this->sanitize_description( $promotion['description'], $type ); } // Sanitize footnote (same as spotlight description - allows light HTML). if ( isset( $promotion['footnote'] ) ) { $promotion['footnote'] = $this->sanitize_description( $promotion['footnote'], 'spotlight' ); } return $promotion; } /** * Sanitize description field based on promotion type. * * Spotlight type allows light HTML: paragraphs, bold, italic, links, breaks. * Badge type only allows links. * * @param string $description The description to sanitize. * @param string $type The promotion type. * * @return string Sanitized description. */ private function sanitize_description( string $description, string $type ): string { if ( 'spotlight' === $type ) { // Allow light HTML for spotlight: paragraphs, bold, italic, links, breaks. $allowed_html = [ 'p' => [], 'strong' => [], 'b' => [], 'em' => [], 'i' => [], 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], 'br' => [], ]; return wp_kses( $description, $allowed_html ); } if ( 'badge' === $type ) { // Badge type: only allow links. $allowed_html = [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [], ], ]; return wp_kses( $description, $allowed_html ); } // Default: strip all HTML. return sanitize_text_field( $description ); } /** * Get the human-readable title for a payment method. * * @param string $payment_method_id The payment method ID. * * @return string The payment method title or a fallback. */ private function get_payment_method_title( string $payment_method_id ): string { $payment_method = WC_Payments::get_payment_method_by_id( $payment_method_id ); if ( false !== $payment_method && method_exists( $payment_method, 'get_title' ) ) { return $payment_method->get_title(); } // Fallback to formatted ID (e.g., 'klarna' -> 'Klarna'). return ucfirst( str_replace( '_', ' ', $payment_method_id ) ); } /** * Send a Tracks event. * * By default Woo adds `url`, `blog_lang`, `blog_id`, `store_id`, `products_count`, and `wc_version` * properties to every event. * * @todo This is a duplicate of the one in the WC_Payments_Account, WC_REST_Payments_Settings_Controller, and WC_Payments_Onboarding_Service classes. * * @param string $name The event name. * @param array $properties Optional. The event custom properties. * * @return void */ private function tracks_event( string $name, array $properties = [] ) { if ( ! function_exists( 'wc_admin_record_tracks_event' ) ) { return; } // Add default properties to every event. $account_service = WC_Payments::get_account_service(); $tracking_info = $account_service ? $account_service->get_tracking_info() : []; $properties = array_merge( $properties, [ 'is_test_mode' => WC_Payments::mode()->is_test(), 'jetpack_connected' => true, // Any PM promotions require a Jetpack connection. 'wcpay_version' => WCPAY_VERSION_NUMBER, 'woo_country_code' => WC()->countries->get_base_country(), ], $tracking_info ?? [] ); wc_admin_record_tracks_event( $name, $properties ); Logger::info( 'Tracks event: ' . $name . ' with data: ' . wp_json_encode( WC_Payments_Utils::redact_array( $properties, [ 'woo_country_code' ] ) ) ); } } includes/class-wc-payment-token-wcpay-link.php 0000777 00000005473 15251156605 0015472 0 ustar 00 <?php /** * Class WC_Payment_Token_WCPay_Link * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * WooCommerce Stripe Link Payment Token. * * Representation of a payment token for Link. * * @class WC_Payment_Token_WCPay_Link */ class WC_Payment_Token_WCPay_Link extends WC_Payment_Token { /** * Class Constant so other code can be unambiguous. * * @type string */ const TYPE = 'wcpay_link'; /** * The payment method type of this token. * * @var string */ protected $type = self::TYPE; /** * Stores Link payment token data. * * @var array */ protected $extra_data = [ 'email' => '', ]; /** * Get payment method type to display to user. * * @param string $deprecated Deprecated since WooCommerce 3.0. * @return string */ public function get_display_name( $deprecated = '' ) { $display = sprintf( /* translators: customer email */ __( 'Stripe Link email ending in %s', 'woocommerce-payments' ), $this->get_redacted_email() ); return $display; } /** * Hook prefix. */ protected function get_hook_prefix() { return 'woocommerce_payments_token_wcpay_link_get_'; } /** * Returns the customer email. * * @param string $context What the value is for. Valid values are view and edit. * * @return string Customer email. */ public function get_email( $context = 'view' ) { return $this->get_prop( 'email', $context ); } /** * Set the customer email. * * @param string $email Customer email. */ public function set_email( $email ) { $this->set_prop( 'email', $email ); } /** * Returns redacted/shortened customer email * * @param string $context What the value is for. Valid values are view and edit. * @return string Redacted/shortened customer email. */ public function get_redacted_email( $context = 'view' ) { return $this->redact_email_address( $this->get_email( $context ) ); } /** * Returns the type of this payment token (CC, eCheck, or something else). * * @param string $deprecated Deprecated since WooCommerce 3.0. * @return string Payment Token Type (CC, eCheck) */ public function get_type( $deprecated = '' ) { return self::TYPE; } /** * Transforms email address into redacted/shortened format like ***xxxx@x.com. * Using shortened length of four characters will mimic CC last-4 digits of card number. * * @param string $email Email address. * @return string Redacted/shortened email address. */ private function redact_email_address( $email ) { $placeholder = '***'; $shortened_length = 4; list( $handle, $domain ) = explode( '@', $email ); $redacted_handle = strlen( $handle ) > $shortened_length ? substr( $handle, - $shortened_length ) : $handle; return "$placeholder$redacted_handle@$domain"; } } includes/payment-methods/class-upe-payment-method.php 0000777 00000036565 15251156605 0017062 0 ustar 00 <?php /** * UPE Payment Method class * * Handles general functionality for UPE payment methods * * @package WCPay\Payment_Methods */ namespace WCPay\Payment_Methods; use WC_Payments_Utils; use WP_User; use WC_Payments_Token_Service; use WC_Payment_Token_CC; use WC_Payment_Token_WCPay_SEPA; use WC_Payments_Subscriptions_Utilities; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Extendable class for payment methods. * * @template T of \WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface */ class UPE_Payment_Method { use WC_Payments_Subscriptions_Utilities; /** * Payment method definition. * * @var class-string<T>|null */ protected $definition; /** * Stripe key name * * @var string */ protected $stripe_id; /** * Display title * * @var string */ protected $title; /** * Can payment method be saved or reused? * * @var bool */ protected $is_reusable; /** * Instance of WC Payments Token Service to save payment method * * @var WC_Payments_Token_Service */ protected $token_service; /** * Supported presentment currencies for which charges for a payment method can be processed * Empty if all currencies are supported * * @var string[] */ protected $currencies; /** * Should payment method be restricted to only domestic payments. * E.g. only to Stripe's connected account currency. * * @var boolean */ protected $accept_only_domestic_payment = false; /** * Represent payment total limitations for the payment method (per-currency). * * @var array<string,array<string,array<string,int>>> */ protected $limits_per_currency = []; /** * Payment method icon URL * * @var string */ protected $icon_url; /** * Payment method icon URL for dark themes (optional) * * @var string */ protected $dark_icon_url; /** * Is the payment method a BNPL (Buy Now Pay Later) method? * * @var boolean */ protected $is_bnpl = false; /** * Supported customer locations for which charges for a payment method can be processed * Empty if all customer locations are supported * * @var string[] */ protected $countries = []; /** * Create instance of payment method * * @param WC_Payments_Token_Service $token_service Instance of WC_Payments_Token_Service. * @param class-string<T>|null $definition Optional payment method definition class name. */ public function __construct( $token_service, ?string $definition = null ) { $this->token_service = $token_service; $this->definition = $definition; if ( null !== $this->definition ) { // Cache values that don't require context. $this->stripe_id = $this->definition::get_id(); $this->is_reusable = PaymentMethodUtils::is_reusable( $this->definition ); $this->currencies = $this->definition::get_supported_currencies(); $this->accept_only_domestic_payment = PaymentMethodUtils::accepts_only_domestic_payments( $this->definition ); $this->limits_per_currency = $this->definition::get_limits_per_currency(); $this->is_bnpl = PaymentMethodUtils::is_bnpl( $this->definition ); $this->countries = $this->definition::get_supported_countries(); } } /** * Returns payment method ID * * @return string */ public function get_id() { if ( null !== $this->definition ) { return $this->definition::get_id(); } return $this->stripe_id; } /** * Returns payment method title * * @param string|null $account_country Country of merchants account. * @param array|false $payment_details Optional payment details from charge object. * * @return string * * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ public function get_title( ?string $account_country = null, $payment_details = false ) { if ( null !== $this->definition ) { return $this->definition::get_title( $account_country ); } return $this->title; } /** * Returns payment method currencies * * @return array */ public function get_currencies() { return $this->currencies; } /** * Determines whether the payment method is restricted to the Stripe account's currency. * E.g.: Afterpay/Clearpay and Affirm only supports domestic payments; Klarna also implements a simplified version of these market restrictions. * * @return bool */ public function has_domestic_transactions_restrictions() { return $this->accept_only_domestic_payment; } /** * Returns boolean dependent on whether payment method can be used at checkout. * * Payment method can be used at checkout if: * - If there are payment amount limits, order total is within limits. * - If it is a subscription order, payment method is either reusable, or subscription is manual. * * @param string $account_country Country of merchants account. * @param bool $skip_limits_per_currency_check Whether to skip limits per currency check. * * @return bool */ public function is_enabled_at_checkout( string $account_country, bool $skip_limits_per_currency_check = false ) { // Check if we're in a subscription context (cart checkout, changing payment method, or renewal). $is_subscription_context = $this->is_subscription_item_in_cart() || $this->is_changing_payment_method_for_subscription(); // Also check if we're on the order-pay page for a renewal order. if ( ! $is_subscription_context && is_wc_endpoint_url( 'order-pay' ) && function_exists( 'wcs_order_contains_renewal' ) ) { $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) ); if ( $order && wcs_order_contains_renewal( $order ) ) { $is_subscription_context = true; } } // Reusable methods are always available for subscriptions. Other methods are available if manual renewal is allowed. $are_manual_renewals_accepted = function_exists( 'wcs_is_manual_renewal_enabled' ) && wcs_is_manual_renewal_enabled(); $is_available_for_subscription = $are_manual_renewals_accepted || $this->is_reusable(); $order_is_within_currency_limits = true; // This part ensures that when payment limits for the currency declared, those will be respected (e.g. BNPLs). if ( [] !== $this->limits_per_currency && ! $skip_limits_per_currency_check ) { $order = null; if ( is_wc_endpoint_url( 'order-pay' ) ) { $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) ); $order = is_a( $order, 'WC_Order' ) ? $order : null; } $currency = get_woocommerce_currency(); if ( $order ) { $currency = $order->get_currency(); } // If the currency limits are not defined, we allow the PM for now (gateway has similar validation for limits). $total = null; if ( $order ) { $total = $order->get_total(); } elseif ( isset( WC()->cart ) ) { $total = WC()->cart->get_total( '' ); } if ( isset( $this->limits_per_currency[ $currency ], WC()->cart ) && ! empty( $total ) ) { $amount = WC_Payments_Utils::prepare_amount( $total, $currency ); if ( $amount > 0 ) { $range = null; if ( isset( $this->limits_per_currency[ $currency ][ $account_country ] ) ) { $range = $this->limits_per_currency[ $currency ][ $account_country ]; } elseif ( isset( $this->limits_per_currency[ $currency ]['default'] ) ) { $range = $this->limits_per_currency[ $currency ]['default']; } // If there is no range specified for the currency-country pair we don't support it and return false. if ( null === $range ) { $order_is_within_currency_limits = false; } else { $is_valid_minimum = null === $range['min'] || $amount >= $range['min']; $is_valid_maximum = null === $range['max'] || $amount <= $range['max']; $order_is_within_currency_limits = $is_valid_minimum && $is_valid_maximum; } } } } return $order_is_within_currency_limits && ( ( ! $is_subscription_context ) || $is_available_for_subscription ); } /** * Returns boolean dependent on whether payment method * will support saved payments/subscription payments * * @return bool */ public function is_reusable() { return $this->is_reusable; } /** * Returns boolean dependent on whether payment method * will support BNPL (Buy Now Pay Later) payments * * @return bool */ public function is_bnpl() { return $this->is_bnpl; } /** * Returns boolean dependent on whether payment method will accept charges * with chosen currency * * @param string $account_domestic_currency Domestic currency of the account. * @param int|null $order_id Optional order ID, if order currency should take precedence. * * @return bool */ public function is_currency_valid( string $account_domestic_currency, $order_id = null ) { $current_store_currency = $this->get_currency( $order_id ); if ( null === $current_store_currency ) { return false; } if ( $this->has_domestic_transactions_restrictions() ) { if ( strtolower( $current_store_currency ) !== strtolower( $account_domestic_currency ) ) { return false; } } $supported_currencies = $this->get_currencies(); return empty( $supported_currencies ) || in_array( $current_store_currency, $supported_currencies, true ); } /** * Add payment method to user and return WC payment token * * @param WP_User $user User to get payment token from. * @param string $payment_method_id Stripe payment method ID string. * * @return WC_Payment_Token_CC|WC_Payment_Token_WCPay_SEPA WC object for payment token. */ public function get_payment_token_for_user( $user, $payment_method_id ) { return $this->token_service->add_payment_method_to_user( $payment_method_id, $user ); } /** * Returns testing credentials to be printed at checkout in test mode. * * @param string $account_country The country of the account. * @return string */ public function get_testing_instructions( string $account_country ) { if ( null !== $this->definition ) { return $this->definition::get_testing_instructions( $account_country ); } return ''; } /** * Returns the payment method icon URL or an empty string. * * @param string|null $account_country Optional account country. * @return string * * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ public function get_icon( ?string $account_country = null ) { if ( null !== $this->definition ) { return $this->definition::get_icon_url( $account_country ); } return isset( $this->icon_url ) ? $this->icon_url : ''; } /** * Returns icon to use on dark themes. * * @param string|null $account_country Optional account country. * @return string */ public function get_dark_icon( ?string $account_country = null ) { if ( null !== $this->definition ) { return $this->definition::get_dark_icon_url( $account_country ); } return isset( $this->dark_icon_url ) ? $this->dark_icon_url : $this->get_icon( $account_country ); } /** * Gets the theme appropriate icon for the payment method for a given location and context. * * @param string $location The location to get the icon for. * @param boolean $is_blocks Whether the icon is for blocks. * @param string $account_country Optional account country. * @return string */ public function get_payment_method_icon_for_location( string $location = 'checkout', bool $is_blocks = true, ?string $account_country = null ) { $appearance_theme = WC_Payments_Utils::get_active_upe_theme_transient_for_location( $location, $is_blocks ? 'blocks' : 'classic' ); if ( 'night' === $appearance_theme ) { return $this->get_dark_icon( $account_country ); } return $this->get_icon( $account_country ); } /** * Returns payment method supported countries * * @return array */ public function get_countries() { $account = \WC_Payments::get_account_service()->get_cached_account_data(); $account_country = isset( $account['country'] ) ? strtoupper( $account['country'] ) : ''; // For definition-based payment methods, call get_supported_countries with account_country. // The definition handles any domestic/regional restrictions (e.g., Klarna's EEA cross-border logic). if ( ! empty( $this->definition ) ) { return $this->definition::get_supported_countries( $account_country ); } return $this->has_domestic_transactions_restrictions() ? [ $account_country ] : $this->countries; } /** * Returns payment method description for the settings page. * * @param string|null $account_country Country of merchants account. * * @return string */ public function get_description( ?string $account_country = null ) { if ( null !== $this->definition ) { return $this->definition::get_description( $account_country ); } return ''; } /** * Returns payment method settings label. * * @param string $account_country Country of merchants account. * @return string */ public function get_settings_label( string $account_country ) { if ( null !== $this->definition ) { return $this->definition::get_settings_label( $account_country ); } return $this->get_title( $account_country ); } /** * Returns payment method settings icon. * * @param string|null $account_country Country of merchants account. * @return string */ public function get_settings_icon_url( ?string $account_country = null ) { if ( null !== $this->definition ) { return $this->definition::get_settings_icon_url( $account_country ); } return $this->get_icon( $account_country ); } /** * Returns boolean dependent on whether payment method allows manual capture. * * @return bool */ public function allows_manual_capture() { if ( null !== $this->definition ) { return PaymentMethodUtils::allows_manual_capture( $this->definition ); } return false; } /** * Returns the Stripe key for the payment method. * * @return string */ public function get_stripe_key() { if ( null !== $this->definition ) { return $this->definition::get_stripe_id(); } return \WC_Payments::get_gateway()->get_payment_method_capability_key_map()[ $this->stripe_id ]; } /** * Returns payment method settings definition. * * @param string $account_country Country of merchants account. * @return array */ public function get_payment_method_information_object( string $account_country ) { return [ 'id' => $this->get_id(), 'label' => $this->get_settings_label( $account_country ), 'description' => $this->get_description( $account_country ), 'settings_icon_url' => $this->get_settings_icon_url( $account_country ), 'currencies' => $this->get_currencies(), 'stripe_key' => $this->get_stripe_key(), 'allows_manual_capture' => $this->allows_manual_capture(), 'allows_pay_later' => $this->is_bnpl(), 'accepts_only_domestic_payment' => $this->has_domestic_transactions_restrictions(), ]; } /** * Returns valid currency to use to filter payment methods. * * @param int $order_id Optional order ID, if order currency should take precedence. * * @return string|null */ private function get_currency( $order_id = null ) { if ( is_wc_endpoint_url( 'order-pay' ) || null !== $order_id ) { global $wp; if ( null === $order_id ) { $order_id = absint( $wp->query_vars['order-pay'] ); } $order = wc_get_order( $order_id ); if ( false === $order ) { return null; } return $order->get_currency(); } return get_woocommerce_currency(); } } includes/payment-methods/class-cc-payment-method.php 0000777 00000007227 15251156605 0016647 0 ustar 00 <?php /** * Class CC_Payment_Method * * @package WCPay\Payment_Methods */ namespace WCPay\Payment_Methods; use WC_Payments_Token_Service; use WCPay\Constants\Country_Test_Cards; /** * Credit card Payment Method class extending UPE base class */ class CC_Payment_Method extends UPE_Payment_Method { const PAYMENT_METHOD_STRIPE_ID = 'card'; /** * Constructor for card payment method * * @param WC_Payments_Token_Service $token_service Token class instance. */ public function __construct( $token_service ) { parent::__construct( $token_service ); $this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID; $this->is_reusable = true; $this->currencies = [];// All currencies are supported. $this->icon_url = plugins_url( 'assets/images/payment-methods/generic-card.svg', WCPAY_PLUGIN_FILE ); } /** * Returns payment method title * * @param string|null $account_country Account country. * @param array|false $payment_details Payment details. * @return string */ public function get_title( ?string $account_country = null, $payment_details = false ) { if ( ! $payment_details ) { return __( 'Card', 'woocommerce-payments' ); } $details = $payment_details[ $this->stripe_id ]; $funding_types = [ 'credit' => __( 'credit', 'woocommerce-payments' ), 'debit' => __( 'debit', 'woocommerce-payments' ), 'prepaid' => __( 'prepaid', 'woocommerce-payments' ), 'unknown' => __( 'unknown', 'woocommerce-payments' ), ]; $card_network = $details['display_brand'] ?? $details['network'] ?? $details['networks']['preferred'] ?? $details['networks']['available'][0]; // Networks like `cartes_bancaires` may use underscores, so we replace them with spaces. $card_network = str_replace( '_', ' ', $card_network ); $payment_method_title = sprintf( // Translators: %1$s card brand, %2$s card funding (prepaid, credit, etc.). __( '%1$s %2$s card', 'woocommerce-payments' ), ucwords( $card_network ), $funding_types[ $details['funding'] ] ); return $payment_method_title; } /** * Returns testing credentials to be printed at checkout in test mode. * * @param string $account_country The country of the account. * @return string */ public function get_testing_instructions( string $account_country ) { $test_card_number = Country_Test_Cards::get_test_card_for_country( $account_country ); return sprintf( // Translators: %s is a test card number. __( 'Use test card <number>%s</number> or refer to our <a>testing guide</a>.', 'woocommerce-payments' ), $test_card_number ); } /** * Returns payment method description for the settings page. * * @param string|null $account_country Country of merchants account. * * @return string */ public function get_description( ?string $account_country = null ) { return __( 'Let your customers pay with major credit and debit cards without leaving your store.', 'woocommerce-payments' ); } /** * Returns payment method settings label. * * @param string $account_country Country of merchants account. * @return string */ public function get_settings_label( string $account_country ) { return __( 'Credit / Debit Cards', 'woocommerce-payments' ); } /** * Returns payment method settings icon. * * @param string|null $account_country Country of merchants account. * @return string */ public function get_settings_icon_url( ?string $account_country = null ) { return plugins_url( 'assets/images/payment-methods/generic-card-black.svg', WCPAY_PLUGIN_FILE ); } /** * Returns boolean dependent on whether payment method allows manual capture. * * @return bool */ public function allows_manual_capture() { return true; } } includes/payment-methods/Configs/Utils/PaymentMethodUtils.php 0000777 00000012204 15251156605 0020504 0 ustar 00 <?php /** * Payment Method Utilities * * @package WCPay\PaymentMethods\Configs\Utils */ namespace WCPay\PaymentMethods\Configs\Utils; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\PaymentMethods\Configs\Registry\PaymentMethodDefinitionRegistry; /** * Utility class for payment method related functions. */ class PaymentMethodUtils { /** * Get the Stripe payment method ID. * By default, this appends '_payments' to the payment method ID. * * @param string $payment_method_id The payment method ID. * @return string */ public static function get_stripe_id( string $payment_method_id ): string { return $payment_method_id . '_payments'; } /** * Whether a payment method is available for the given currency and country * * @param array<string> $supported_currencies The list of supported currencies. * @param array<string> $supported_countries The list of supported countries. * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * @return bool */ public static function is_available_for( array $supported_currencies, array $supported_countries, string $currency, string $account_country ): bool { // Check if currency is supported. if ( ! empty( $supported_currencies ) && ! in_array( $currency, $supported_currencies, true ) ) { return false; } // Check if country is supported. if ( ! empty( $supported_countries ) && ! in_array( $account_country, $supported_countries, true ) ) { return false; } return true; } /** * Is the payment method a BNPL (Buy Now Pay Later) payment method? * * @param string $payment_method_definition The payment method definition class name. * @return boolean */ public static function is_bnpl( string $payment_method_definition ): bool { return in_array( PaymentMethodCapability::BUY_NOW_PAY_LATER, $payment_method_definition::get_capabilities(), true ); } /** * Is the payment method a reusable payment method? * * @param string $payment_method_definition The payment method definition class name. * @return boolean */ public static function is_reusable( string $payment_method_definition ): bool { return in_array( PaymentMethodCapability::TOKENIZATION, $payment_method_definition::get_capabilities(), true ); } /** * Does the payment method accept only domestic payments? * * @param string $payment_method_definition The payment method definition class name. * @return boolean */ public static function accepts_only_domestic_payments( string $payment_method_definition ): bool { return in_array( PaymentMethodCapability::DOMESTIC_TRANSACTIONS_ONLY, $payment_method_definition::get_capabilities(), true ); } /** * Does the payment method allow manual capture? * * @param string $payment_method_definition The payment method definition class name. * @return boolean */ public static function allows_manual_capture( string $payment_method_definition ): bool { return in_array( PaymentMethodCapability::CAPTURE_LATER, $payment_method_definition::get_capabilities(), true ); } /** * Checks if a currency is domestic for a given country. * * @param string $currency The currency code to check. * @param string $country The country code to check against. * @return bool True if the currency is domestic for the country */ public static function is_domestic_currency_for_country( string $currency, string $country ): bool { // Get the locale info which contains country->currency mapping. $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; // If country doesn't exist in our locale info, we can't validate. if ( ! isset( $locale_info[ $country ] ) ) { return false; } return $locale_info[ $country ]['currency_code'] === $currency; } /** * Get the payment method definitions as a JSON string. * * @return string */ public static function get_payment_method_definitions_json() { $registry = PaymentMethodDefinitionRegistry::instance(); $payment_method_definitions = []; foreach ( $registry->get_available_definitions() as $payment_method_definition ) { $payment_method_definitions[ $payment_method_definition::get_id() ] = [ 'id' => $payment_method_definition::get_id(), 'stripe_key' => $payment_method_definition::get_stripe_id(), 'title' => $payment_method_definition::get_title(), 'description' => $payment_method_definition::get_description(), 'settings_icon_url' => $payment_method_definition::get_settings_icon_url(), 'currencies' => $payment_method_definition::get_supported_currencies(), 'allows_manual_capture' => self::allows_manual_capture( $payment_method_definition ), 'allows_pay_later' => self::is_bnpl( $payment_method_definition ), 'accepts_only_domestic_payment' => self::accepts_only_domestic_payments( $payment_method_definition ), ]; } $encoded_response = wp_json_encode( $payment_method_definitions ); return false === $encoded_response ? '' : $encoded_response; } } includes/payment-methods/Configs/Registry/PaymentMethodDefinitionRegistry.php 0000777 00000013073 15251156605 0023742 0 ustar 00 <?php /** * Payment method definition registry. * * @package WCPay\PaymentMethods\Configs\Registry */ namespace WCPay\PaymentMethods\Configs\Registry; use WCPay\PaymentMethods\Configs\Definitions\AffirmDefinition; use WCPay\PaymentMethods\Configs\Definitions\AfterpayDefinition; use WCPay\PaymentMethods\Configs\Definitions\AlipayDefinition; use WCPay\PaymentMethods\Configs\Definitions\AmazonPayDefinition; use WCPay\PaymentMethods\Configs\Definitions\ApplePayDefinition; use WCPay\PaymentMethods\Configs\Definitions\BancontactDefinition; use WCPay\PaymentMethods\Configs\Definitions\BecsDefinition; use WCPay\PaymentMethods\Configs\Definitions\EpsDefinition; use WCPay\PaymentMethods\Configs\Definitions\GiropayDefinition; use WCPay\PaymentMethods\Configs\Definitions\GooglePayDefinition; use WCPay\PaymentMethods\Configs\Definitions\GrabPayDefinition; use WCPay\PaymentMethods\Configs\Definitions\IdealDefinition; use WCPay\PaymentMethods\Configs\Definitions\KlarnaDefinition; use WCPay\PaymentMethods\Configs\Definitions\LinkDefinition; use WCPay\PaymentMethods\Configs\Definitions\MultibancoDefinition; use WCPay\PaymentMethods\Configs\Definitions\P24Definition; use WCPay\PaymentMethods\Configs\Definitions\SepaDefinition; use WCPay\PaymentMethods\Configs\Definitions\SofortDefinition; use WCPay\PaymentMethods\Configs\Definitions\WechatPayDefinition; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; /** * Registry for payment method definitions. */ class PaymentMethodDefinitionRegistry { /** * Singleton instance. * * @var PaymentMethodDefinitionRegistry|null */ private static $instance = null; /** * List of all available payment method definitions. * * @var array<class-string<PaymentMethodDefinitionInterface>> */ private $available_definitions = [ // Add new payment method definitions here. AffirmDefinition::class, AfterpayDefinition::class, AlipayDefinition::class, BancontactDefinition::class, BecsDefinition::class, EpsDefinition::class, GiropayDefinition::class, GrabPayDefinition::class, IdealDefinition::class, LinkDefinition::class, MultibancoDefinition::class, KlarnaDefinition::class, P24Definition::class, SepaDefinition::class, SofortDefinition::class, WechatPayDefinition::class, ApplePayDefinition::class, GooglePayDefinition::class, ]; /** * Payment method definitions that have been registered for use. * * @var array<string,class-string<PaymentMethodDefinitionInterface>> */ private $payment_methods = []; /** * Constructor is private to enforce singleton pattern. */ private function __construct() {} /** * Get the singleton instance. * * @return PaymentMethodDefinitionRegistry */ public static function instance(): self { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Initialize the registry by registering all available payment method definitions. * * @return void */ public function init(): void { foreach ( $this->available_definitions as $definition ) { $this->register_payment_method( $definition ); } // When Amazon Pay is promoted from feature flag, we can register it directly in the `available_definitions` array. if ( \WC_Payments_Features::is_amazon_pay_enabled() ) { $this->register_payment_method( AmazonPayDefinition::class ); } } /** * Get all available payment method definitions. * * @return array<class-string<PaymentMethodDefinitionInterface>> Array of payment method definition class names. */ public function get_available_definitions(): array { return $this->available_definitions; } /** * Register a payment method definition. * * @param class-string<PaymentMethodDefinitionInterface> $definition_class The payment method definition class to register. * @throws \InvalidArgumentException If the class does not exist or does not implement PaymentMethodDefinitionInterface. */ public function register_payment_method( $definition_class ): void { if ( ! class_exists( $definition_class ) ) { throw new \InvalidArgumentException( sprintf( 'Payment method definition class "%s" does not exist.', $definition_class ) ); } $interfaces = class_implements( $definition_class ); if ( ! isset( $interfaces[ PaymentMethodDefinitionInterface::class ] ) ) { throw new \InvalidArgumentException( sprintf( 'Payment method definition class "%s" must implement %s.', $definition_class, PaymentMethodDefinitionInterface::class ) ); } /** * Ensure type safety for the payment method definition class. * * @var class-string<PaymentMethodDefinitionInterface> $definition_class */ $this->payment_methods[ $definition_class::get_id() ] = $definition_class; } /** * Get all registered payment method definitions. * * @return class-string<PaymentMethodDefinitionInterface>[] All registered payment method definition classes. */ public function get_all_payment_method_definitions(): array { return $this->payment_methods; } /** * Get all available payment method definitions for a given account and currency. * * @param string $account_country The account country. * @param string $currency The currency. * @return string[] All available payment method definition classes. */ public function get_available_payment_method_definitions( string $account_country, string $currency ): array { return array_filter( $this->payment_methods, function ( $definition_class ) use ( $account_country, $currency ) { return $definition_class::is_available_for( $currency, $account_country ); } ); } } includes/payment-methods/Configs/Definitions/LinkDefinition.php 0000777 00000012773 15251156605 0021001 0 ustar 00 <?php /** * Link Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Link payment method definition. */ class LinkDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'link'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'link', 'stripe link' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Link', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Link autofills your customers\' payment and shipping details to deliver an easy and seamless checkout experience.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::UNITED_STATES_DOLLAR ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return []; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, PaymentMethodCapability::TOKENIZATION, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/link.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/P24Definition.php 0000777 00000013022 15251156605 0020435 0 ustar 00 <?php /** * Przelewy24 (P24) Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Przelewy24 (P24) payment method definition. */ class P24Definition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'p24'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'p24', 'przelewy24' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Przelewy24 (P24)', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Accept payments with Przelewy24 (P24), the most popular payment method in Poland.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::EURO, Currency_Code::POLISH_ZLOTY, ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return [ Country_Code::POLAND ]; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/p24.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/IdealDefinition.php 0000777 00000012717 15251156605 0021120 0 ustar 00 <?php /** * IDEAL Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the iDEAL payment method definition. */ class IdealDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'ideal'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'ideal' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'iDEAL', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Expand your business with iDEAL — Netherlands\'s most popular payment method.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::EURO ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return [ Country_Code::NETHERLANDS ]; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/ideal.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country . * * @return string HTML string containing testing instructions . */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/EpsDefinition.php 0000777 00000012653 15251156605 0020630 0 ustar 00 <?php /** * EPS Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the EPS payment method definition. */ class EpsDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'eps'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'eps' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'EPS', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Accept your payment with EPS — a common payment method in Austria.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::EURO ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return [ Country_Code::AUSTRIA ]; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/eps.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/BancontactDefinition.php 0000777 00000012776 15251156605 0022163 0 ustar 00 <?php /** * Bancontact Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Bancontact payment method definition. */ class BancontactDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'bancontact'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'bancontact' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Bancontact', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Bancontact is a bank redirect payment method offered by more than 80% of online businesses in Belgium.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::EURO ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return [ Country_Code::BELGIUM ]; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/bancontact.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/SepaDefinition.php 0000777 00000015563 15251156605 0020774 0 ustar 00 <?php /** * SEPA Direct Debit Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the SEPA Direct Debit payment method definition. */ class SepaDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'sepa_debit'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'sepa' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'SEPA Direct Debit', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Reach 500 million customers and over 20 million businesses across the European Union.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::EURO ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { // https://stripe.com/en-br/resources/more/sepa-country-list#list-of-sepa-countries. $eu_countries = [ Country_Code::AUSTRIA, Country_Code::BELGIUM, Country_Code::BULGARIA, Country_Code::CROATIA, Country_Code::CYPRUS, Country_Code::CZECHIA, Country_Code::DENMARK, Country_Code::ESTONIA, Country_Code::FINLAND, Country_Code::FRANCE, Country_Code::GERMANY, Country_Code::GREECE, Country_Code::HUNGARY, Country_Code::IRELAND, Country_Code::ITALY, Country_Code::LATVIA, Country_Code::LITHUANIA, Country_Code::LUXEMBOURG, Country_Code::MALTA, Country_Code::NETHERLANDS, Country_Code::POLAND, Country_Code::PORTUGAL, Country_Code::ROMANIA, Country_Code::SLOVAKIA, Country_Code::SLOVENIA, Country_Code::SPAIN, Country_Code::SWEDEN, ]; $additional_sepa_countries = [ Country_Code::SWITZERLAND, Country_Code::UNITED_KINGDOM, Country_Code::SAN_MARINO, Country_Code::VATICAN_CITY, Country_Code::ANDORRA, Country_Code::MONACO, Country_Code::LIECHTENSTEIN, Country_Code::NORWAY, Country_Code::ICELAND, ]; return array_merge( $eu_countries, $additional_sepa_countries ); } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/sepa-debit.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return __( '<strong>Test mode:</strong> use the test account number <number>AT611904300234573201</number>. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed <a>here</a>.', 'woocommerce-payments' ); } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/AffirmDefinition.php 0000777 00000015277 15251156605 0021312 0 ustar 00 <?php /** * Affirm Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Affirm payment method definition. */ class AffirmDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'affirm'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'affirm' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Affirm', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Allow customers to pay over time with Affirm.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::UNITED_STATES_DOLLAR, Currency_Code::CANADIAN_DOLLAR, ]; } /** * Get the list of supported countries. * * Affirm only supports domestic transactions, so when account_country is provided, * only that country is returned if it's in the supported list. * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { $supported_countries = [ Country_Code::UNITED_STATES, Country_Code::CANADA, ]; // Affirm only supports domestic transactions. if ( null !== $account_country && in_array( strtoupper( $account_country ), $supported_countries, true ) ) { return [ strtoupper( $account_country ) ]; } return $supported_countries; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::BUY_NOW_PAY_LATER, PaymentMethodCapability::REFUNDS, PaymentMethodCapability::DOMESTIC_TRANSACTIONS_ONLY, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/affirm-logo.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/affirm-logo-dark.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/affirm-badge.svg', WCPAY_PLUGIN_FILE ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return [ Currency_Code::CANADIAN_DOLLAR => [ Country_Code::CANADA => [ 'min' => 5000, 'max' => 3000000, ], ], Currency_Code::UNITED_STATES_DOLLAR => [ Country_Code::UNITED_STATES => [ 'min' => 5000, 'max' => 3000000, ], ], ]; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { $limits = self::get_limits_per_currency(); if ( isset( $limits[ $currency ][ $country ]['min'] ) ) { return $limits[ $currency ][ $country ]['min']; } return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { $limits = self::get_limits_per_currency(); if ( isset( $limits[ $currency ][ $country ]['max'] ) ) { return $limits[ $currency ][ $country ]['max']; } return null; } } includes/payment-methods/Configs/Definitions/AmazonPayDefinition.php 0000777 00000014412 15251156605 0021773 0 ustar 00 <?php /** * Amazon Pay Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Amazon Pay payment method definition. */ class AmazonPayDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'amazon_pay'; } /** * Get the keywords for the payment method. These are used by the duplicates detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'amazon_pay', 'amazonpay', 'amazon' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Amazon Pay', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Offer customers a fast, secure checkout experience with Amazon Pay.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { $account = \WC_Payments::get_account_service()->get_cached_account_data(); $account_country = isset( $account['country'] ) ? strtoupper( $account['country'] ) : ''; if ( Country_Code::UNITED_STATES === $account_country ) { return [ Currency_Code::UNITED_STATES_DOLLAR ]; } return [ Currency_Code::UNITED_STATES_DOLLAR, Currency_Code::AUSTRALIAN_DOLLAR, Currency_Code::POUND_STERLING, Currency_Code::DANISH_KRONE, Currency_Code::EURO, Currency_Code::HONG_KONG_DOLLAR, Currency_Code::JAPANESE_YEN, Currency_Code::NEW_ZEALAND_DOLLAR, Currency_Code::NORWEGIAN_KRONE, Currency_Code::SWEDISH_KRONA, Currency_Code::SWISS_FRANC, Currency_Code::SOUTH_AFRICAN_RAND, ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return []; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, PaymentMethodCapability::MULTI_CURRENCY, PaymentMethodCapability::TOKENIZATION, PaymentMethodCapability::CAPTURE_LATER, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/amazon-pay.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/KlarnaDefinition.php 0000777 00000020546 15251156605 0021311 0 ustar 00 <?php /** * Klarna Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WC_Payments_Utils; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Klarna payment method definition. */ class KlarnaDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'klarna'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'klarna' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Klarna', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Allow customers to pay over time or pay now with Klarna.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::UNITED_STATES_DOLLAR, Currency_Code::POUND_STERLING, Currency_Code::EURO, Currency_Code::DANISH_KRONE, Currency_Code::NORWEGIAN_KRONE, Currency_Code::SWEDISH_KRONA, ]; } /** * Get the list of supported countries. * * Klarna has domestic transaction restrictions with special handling for EEA merchants: * - Non-EEA merchants (e.g., US): Only domestic transactions allowed (single country). * - EEA/UK/Switzerland merchants: Cross-border within EEA allowed, but only to countries * that share the store's domestic currency. * * @param string|null $account_country Optional. The merchant's account country. * Required for domestic/EEA cross-border logic. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { // All countries where Klarna is available. $all_supported_countries = [ Country_Code::UNITED_STATES, Country_Code::UNITED_KINGDOM, Country_Code::AUSTRIA, Country_Code::GERMANY, Country_Code::NETHERLANDS, Country_Code::BELGIUM, Country_Code::SPAIN, Country_Code::ITALY, Country_Code::IRELAND, Country_Code::DENMARK, Country_Code::FINLAND, Country_Code::NORWAY, Country_Code::SWEDEN, Country_Code::FRANCE, ]; if ( null === $account_country ) { return $all_supported_countries; } $account_country = strtoupper( $account_country ); // Countries in the EEA can transact across all other EEA countries. // This includes Switzerland and the UK who aren't strictly in the EU. $eea_extended_countries = array_merge( WC_Payments_Utils::get_european_economic_area_countries(), [ Country_Code::SWITZERLAND, Country_Code::UNITED_KINGDOM ] ); // If the merchant is NOT in the EEA/UK/Switzerland, apply strict domestic restriction. if ( ! in_array( $account_country, $eea_extended_countries, true ) ) { // Non-EEA merchants can only accept domestic transactions. if ( in_array( $account_country, $all_supported_countries, true ) ) { return [ $account_country ]; } return $all_supported_countries; } // Merchant is in EEA/UK/Switzerland - apply cross-border currency logic. $store_currency = strtoupper( \get_woocommerce_currency() ); $limits = self::get_limits_per_currency(); // If the store currency is not supported by Klarna, return no countries. if ( ! isset( $limits[ $store_currency ] ) ) { return [ 'NONE_SUPPORTED' ]; } // Only countries that support the store's currency are eligible. $countries_supporting_currency = array_keys( $limits[ $store_currency ] ); return array_values( array_intersect( $eea_extended_countries, $countries_supporting_currency ) ); } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::BUY_NOW_PAY_LATER, PaymentMethodCapability::REFUNDS, PaymentMethodCapability::DOMESTIC_TRANSACTIONS_ONLY, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/klarna-pill.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/klarna.svg', WCPAY_PLUGIN_FILE ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return WC_Payments_Utils::get_bnpl_limits_per_currency( self::get_id() ); } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { $limits = self::get_limits_per_currency(); if ( isset( $limits[ $currency ][ $country ]['min'] ) ) { return $limits[ $currency ][ $country ]['min']; } return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { $limits = self::get_limits_per_currency(); if ( isset( $limits[ $currency ][ $country ]['max'] ) ) { return $limits[ $currency ][ $country ]['max']; } return null; } } includes/payment-methods/Configs/Definitions/AlipayDefinition.php 0000777 00000017006 15251156605 0021315 0 ustar 00 <?php /** * Alipay Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Alipay payment method definition. */ class AlipayDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'alipay'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'alipay' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Alipay', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'A digital wallet for customers with mainland China Alipay accounts. Regional versions like AlipayHK are not supported.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { $account = \WC_Payments::get_account_service()->get_cached_account_data(); $account_country = isset( $account['country'] ) ? strtoupper( $account['country'] ) : ''; if ( Country_Code::AUSTRALIA === $account_country ) { return [ Currency_Code::AUSTRALIAN_DOLLAR ]; } if ( Country_Code::CANADA === $account_country ) { return [ Currency_Code::CANADIAN_DOLLAR ]; } if ( Country_Code::UNITED_KINGDOM === $account_country ) { return [ Currency_Code::POUND_STERLING ]; } if ( Country_Code::HONG_KONG === $account_country ) { return [ Currency_Code::HONG_KONG_DOLLAR ]; } if ( Country_Code::JAPAN === $account_country ) { return [ Currency_Code::JAPANESE_YEN ]; } if ( Country_Code::NEW_ZEALAND === $account_country ) { return [ Currency_Code::NEW_ZEALAND_DOLLAR ]; } if ( Country_Code::SINGAPORE === $account_country ) { return [ Currency_Code::SINGAPORE_DOLLAR ]; } if ( Country_Code::UNITED_STATES === $account_country ) { return [ Currency_Code::UNITED_STATES_DOLLAR ]; } if ( Country_Code::HUNGARY === $account_country ) { return [ Currency_Code::HUNGARIAN_FORINT ]; } if ( in_array( $account_country, [ Country_Code::AUSTRIA, Country_Code::BELGIUM, Country_Code::BULGARIA, Country_Code::CYPRUS, Country_Code::CZECHIA, Country_Code::DENMARK, Country_Code::ESTONIA, Country_Code::FINLAND, Country_Code::FRANCE, Country_Code::GERMANY, Country_Code::GREECE, Country_Code::IRELAND, Country_Code::ITALY, Country_Code::LATVIA, Country_Code::LITHUANIA, Country_Code::LUXEMBOURG, Country_Code::MALTA, Country_Code::NETHERLANDS, Country_Code::NORWAY, Country_Code::PORTUGAL, Country_Code::ROMANIA, Country_Code::SLOVAKIA, Country_Code::SLOVENIA, Country_Code::SPAIN, Country_Code::SWEDEN, Country_Code::SWITZERLAND, Country_Code::CROATIA, ], true ) ) { return [ Currency_Code::EURO ]; } // fallback currency code, just in case. return [ Currency_Code::CHINESE_YUAN ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return []; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, PaymentMethodCapability::MULTI_CURRENCY, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/alipay-logo.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/WechatPayDefinition.php 0000777 00000017643 15251156605 0021772 0 ustar 00 <?php /** * WeChat Pay Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the WeChat Pay payment method definition. */ class WechatPayDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'wechat_pay'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'wechat_pay', 'wechatpay' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'WeChat Pay', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'A digital wallet for customers with mainland China WeChat Pay wallets. Regional versions like WeChat Pay HK are not supported.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { $account = \WC_Payments::get_account_service()->get_cached_account_data(); $account_country = isset( $account['country'] ) ? strtoupper( $account['country'] ) : ''; // For all European countries in the supported list, return EUR. if ( in_array( $account_country, [ Country_Code::AUSTRIA, Country_Code::BELGIUM, Country_Code::FINLAND, Country_Code::FRANCE, Country_Code::GERMANY, Country_Code::IRELAND, Country_Code::ITALY, Country_Code::LUXEMBOURG, Country_Code::NETHERLANDS, Country_Code::PORTUGAL, Country_Code::SPAIN, ], true ) ) { return [ Currency_Code::EURO ]; } if ( Country_Code::AUSTRALIA === $account_country ) { return [ Currency_Code::AUSTRALIAN_DOLLAR ]; } if ( Country_Code::CANADA === $account_country ) { return [ Currency_Code::CANADIAN_DOLLAR ]; } if ( Country_Code::DENMARK === $account_country ) { return [ Currency_Code::DANISH_KRONE ]; } if ( Country_Code::HONG_KONG === $account_country ) { return [ Currency_Code::HONG_KONG_DOLLAR ]; } if ( Country_Code::JAPAN === $account_country ) { return [ Currency_Code::JAPANESE_YEN ]; } if ( Country_Code::NORWAY === $account_country ) { return [ Currency_Code::NORWEGIAN_KRONE ]; } if ( Country_Code::SINGAPORE === $account_country ) { return [ Currency_Code::SINGAPORE_DOLLAR ]; } if ( Country_Code::SWEDEN === $account_country ) { return [ Currency_Code::SWEDISH_KRONA ]; } if ( Country_Code::SWITZERLAND === $account_country ) { return [ Currency_Code::SWISS_FRANC ]; } if ( Country_Code::UNITED_KINGDOM === $account_country ) { return [ Currency_Code::POUND_STERLING ]; } if ( Country_Code::UNITED_STATES === $account_country ) { return [ Currency_Code::UNITED_STATES_DOLLAR ]; } return [ 'NONE_SUPPORTED' ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return [ Country_Code::UNITED_STATES, Country_Code::AUSTRALIA, Country_Code::CANADA, Country_Code::AUSTRIA, Country_Code::BELGIUM, Country_Code::DENMARK, Country_Code::FINLAND, Country_Code::FRANCE, Country_Code::GERMANY, Country_Code::IRELAND, Country_Code::ITALY, Country_Code::LUXEMBOURG, Country_Code::NETHERLANDS, Country_Code::NORWAY, Country_Code::PORTUGAL, Country_Code::SPAIN, Country_Code::SWEDEN, Country_Code::SWITZERLAND, Country_Code::UNITED_KINGDOM, Country_Code::HONG_KONG, Country_Code::JAPAN, Country_Code::SINGAPORE, ]; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, PaymentMethodCapability::MULTI_CURRENCY, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/wechat-pay.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/GiropayDefinition.php 0000777 00000012734 15251156605 0021513 0 ustar 00 <?php /** * Giropay Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Giropay payment method definition. */ class GiropayDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'giropay'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'giropay' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'giropay', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Expand your business with giropay — Germany\'s second most popular payment system.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::EURO ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return [ Country_Code::GERMANY ]; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/giropay.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * * @return string HTML string containing testing instructions. */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/MultibancoDefinition.php 0000777 00000013106 15251156605 0022170 0 ustar 00 <?php /** * Multibanco Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Multibanco payment method definition. */ class MultibancoDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'multibanco'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'multibanco' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Multibanco', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'A voucher based payment method for your customers in Portugal.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::EURO ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return [ Country_Code::PORTUGAL ]; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/multibanco-logo.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/multibanco-logo-dark.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/multibanco.svg', WCPAY_PLUGIN_FILE ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/GooglePayDefinition.php 0000777 00000013203 15251156605 0021757 0 ustar 00 <?php /** * Google Pay Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Google Pay payment method definition. */ class GooglePayDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'google_pay'; } /** * Get the keywords for the payment method. These are used by the duplicates detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'google_pay', 'googlepay', 'gpay' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Google Pay', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Offer customers a fast, secure checkout experience with Google Pay.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { // Google Pay supports the same currencies as card payments. // Return all available currencies. return []; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return []; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, PaymentMethodCapability::MULTI_CURRENCY, PaymentMethodCapability::TOKENIZATION, PaymentMethodCapability::CAPTURE_LATER, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/cards/google-pay.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { if ( ! PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ) ) { return false; } return true; } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/ApplePayDefinition.php 0000777 00000013166 15251156605 0021614 0 ustar 00 <?php /** * Apple Pay Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Apple Pay payment method definition. */ class ApplePayDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'apple_pay'; } /** * Get the keywords for the payment method. These are used by the duplicates detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'apple_pay', 'applepay' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Apple Pay', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Apple Pay is an easy and secure way for customers to pay on your store.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { // Apple Pay supports the same currencies as card payments. // Return all available currencies. return []; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return []; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, PaymentMethodCapability::MULTI_CURRENCY, PaymentMethodCapability::TOKENIZATION, PaymentMethodCapability::CAPTURE_LATER, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/cards/apple-pay.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { if ( ! PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ) ) { return false; } return true; } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/BecsDefinition.php 0000777 00000013375 15251156605 0020757 0 ustar 00 <?php /** * BECS Direct Debit Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the BECS Direct Debit payment method definition. */ class BecsDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'au_becs_debit'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'becs' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'BECS Direct Debit', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Bulk Electronic Clearing System — Accept secure bank transfer from Australia.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::AUSTRALIAN_DOLLAR ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return [ Country_Code::AUSTRALIA ]; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/bank-debit.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return __( '<strong>Test mode:</strong> use the test account number <number>000123456</number>. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed <a>here</a>.', 'woocommerce-payments' ); } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/AfterpayDefinition.php 0000777 00000020143 15251156605 0021645 0 ustar 00 <?php /** * Afterpay Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WC_Payments_Utils; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Afterpay payment method definition. */ class AfterpayDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'afterpay_clearpay'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'afterpay', 'clearpay' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { if ( Country_Code::UNITED_KINGDOM === $account_country ) { return __( 'Clearpay', 'woocommerce-payments' ); } if ( Country_Code::UNITED_STATES === $account_country ) { return __( 'Cash App Afterpay', 'woocommerce-payments' ); } return __( 'Afterpay', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { if ( Country_Code::UNITED_KINGDOM === $account_country ) { return __( 'Allow customers to pay over time with Clearpay.', 'woocommerce-payments' ); } if ( Country_Code::UNITED_STATES === $account_country ) { return __( 'Allow customers to pay over time with Cash App Afterpay.', 'woocommerce-payments' ); } return __( 'Allow customers to pay over time with Afterpay.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::UNITED_STATES_DOLLAR, Currency_Code::CANADIAN_DOLLAR, Currency_Code::AUSTRALIAN_DOLLAR, Currency_Code::NEW_ZEALAND_DOLLAR, Currency_Code::POUND_STERLING, ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { // only domestic transactions are supported. $supported_countries = [ Country_Code::UNITED_STATES, Country_Code::CANADA, Country_Code::AUSTRALIA, Country_Code::NEW_ZEALAND, Country_Code::UNITED_KINGDOM, ]; if ( null !== $account_country && in_array( strtoupper( $account_country ), $supported_countries, true ) ) { return [ strtoupper( $account_country ) ]; } return $supported_countries; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::BUY_NOW_PAY_LATER, PaymentMethodCapability::REFUNDS, PaymentMethodCapability::DOMESTIC_TRANSACTIONS_ONLY, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { if ( Country_Code::UNITED_KINGDOM === $account_country ) { return plugins_url( 'assets/images/payment-methods/clearpay.svg', WCPAY_PLUGIN_FILE ); } if ( Country_Code::UNITED_STATES === $account_country ) { return plugins_url( 'assets/images/payment-methods/afterpay-cashapp-logo.svg', WCPAY_PLUGIN_FILE ); } return plugins_url( 'assets/images/payment-methods/afterpay-badge.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { if ( Country_Code::UNITED_KINGDOM === $account_country ) { return plugins_url( 'assets/images/payment-methods/clearpay.svg', WCPAY_PLUGIN_FILE ); } if ( Country_Code::UNITED_STATES === $account_country ) { return plugins_url( 'assets/images/payment-methods/afterpay-cashapp-logo-dark.svg', WCPAY_PLUGIN_FILE ); } return plugins_url( 'assets/images/payment-methods/afterpay-badge.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { if ( Country_Code::UNITED_KINGDOM === $account_country ) { return plugins_url( 'assets/images/payment-methods/clearpay.svg', WCPAY_PLUGIN_FILE ); } if ( Country_Code::UNITED_STATES === $account_country ) { return plugins_url( 'assets/images/payment-methods/afterpay-cashapp-badge.svg', WCPAY_PLUGIN_FILE ); } return plugins_url( 'assets/images/payment-methods/afterpay-logo.svg', WCPAY_PLUGIN_FILE ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return WC_Payments_Utils::get_bnpl_limits_per_currency( self::get_id() ); } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { $limits = self::get_limits_per_currency(); if ( isset( $limits[ $currency ][ $country ]['min'] ) ) { return $limits[ $currency ][ $country ]['min']; } return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { $limits = self::get_limits_per_currency(); if ( isset( $limits[ $currency ][ $country ]['max'] ) ) { return $limits[ $currency ][ $country ]['max']; } return null; } } includes/payment-methods/Configs/Definitions/GrabPayDefinition.php 0000777 00000013464 15251156605 0021427 0 ustar 00 <?php /** * GrabPay Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the GrabPay payment method definition. */ class GrabPayDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'grabpay'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'grabpay', 'grab_pay', 'grab' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'GrabPay', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'A popular digital wallet for cashless payments in Singapore.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * Note: If WooPayments becomes available to merchants from Malaysia in the future, we'll need to not only add MYR here, * but also implement logic to limit the currency based on the Stripe account country, * so SG accounts only accept SGD, and MY accounts only accept MYR. * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::SINGAPORE_DOLLAR ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return [ Country_Code::SINGAPORE ]; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, PaymentMethodCapability::DOMESTIC_TRANSACTIONS_ONLY, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/grabpay.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Definitions/SofortDefinition.php 0000777 00000013107 15251156605 0021350 0 ustar 00 <?php /** * Sofort Payment Method Definition * * @package WCPay\PaymentMethods\Configs\Definitions */ namespace WCPay\PaymentMethods\Configs\Definitions; use WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface; use WCPay\PaymentMethods\Configs\Constants\PaymentMethodCapability; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils; /** * Class implementing the Sofort payment method definition. */ class SofortDefinition implements PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method * * @return string */ public static function get_id(): string { return 'sofort'; } /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array { return [ 'sofort' ]; } /** * Get the Stripe payment method ID * * @return string */ public static function get_stripe_id(): string { return PaymentMethodUtils::get_stripe_id( self::get_id() ); } /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_title( ?string $account_country = null ): string { return __( 'Sofort', 'woocommerce-payments' ); } /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_label( ?string $account_country = null ): string { return self::get_title( $account_country ); } /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string { return __( 'Accept secure bank transfers from Austria, Belgium, Germany, Italy, Netherlands, and Spain.', 'woocommerce-payments' ); } /** * Get the list of supported currencies * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array { return [ Currency_Code::EURO ]; } /** * Get the list of supported countries * * @param string|null $account_country Optional. The merchant's account country. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array { return [ Country_Code::AUSTRIA, Country_Code::BELGIUM, Country_Code::GERMANY, Country_Code::NETHERLANDS, Country_Code::SPAIN, ]; } /** * Get the payment method capabilities * * @return string[] */ public static function get_capabilities(): array { return [ PaymentMethodCapability::REFUNDS, ]; } /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_icon_url( ?string $account_country = null ): string { return plugins_url( 'assets/images/payment-methods/sofort.svg', WCPAY_PLUGIN_FILE ); } /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the URL for the payment method's settings icon * * @param string|null $account_country Optional. The merchant's account country. * * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string { return self::get_icon_url( $account_country ); } /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string { return ''; } /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array { return []; } /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool { return PaymentMethodUtils::is_available_for( self::get_supported_currencies(), self::get_supported_countries( $account_country ), $currency, $account_country ); } /** * Get the minimum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The minimum amount or null if no minimum. */ public static function get_minimum_amount( string $currency, string $country ): ?int { return null; } /** * Get the maximum amount for this payment method for a given currency and country * * @param string $currency The currency code. * @param string $country The country code. * * @return int|null The maximum amount or null if no maximum. */ public static function get_maximum_amount( string $currency, string $country ): ?int { return null; } } includes/payment-methods/Configs/Constants/PaymentMethodCapability.php 0000777 00000002003 15251156605 0022335 0 ustar 00 <?php /** * Payment Method Capability Constants * * @package WCPay\PaymentMethods\Configs\Constants */ namespace WCPay\PaymentMethods\Configs\Constants; /** * Class defining payment method capability constants. */ class PaymentMethodCapability { /** * Payment method can be saved and reused * * @var string */ public const TOKENIZATION = 'tokenization'; /** * Payment method supports refunds * * @var string */ public const REFUNDS = 'refunds'; /** * Payment method supports capturing payment later * * @var string */ public const CAPTURE_LATER = 'capture_later'; /** * Payment method supports multiple currencies * * @var string */ public const MULTI_CURRENCY = 'multi_currency'; /** * Payment method is a Buy Now Pay Later method * * @var string */ public const BUY_NOW_PAY_LATER = 'buy_now_pay_later'; /** * Payment method only accepts domestic transactions * * @var string */ public const DOMESTIC_TRANSACTIONS_ONLY = 'domestic_transactions_only'; } includes/payment-methods/Configs/Interfaces/PaymentMethodDefinitionInterface.php 0000777 00000011605 15251156605 0024304 0 ustar 00 <?php /** * Payment Method Definition Interface * * @package WCPay\PaymentMethods\Configs\Interfaces */ namespace WCPay\PaymentMethods\Configs\Interfaces; /** * Interface for defining payment method configurations. * Provides a single source of truth for both backend and frontend payment method properties. */ interface PaymentMethodDefinitionInterface { /** * Get the internal ID for the payment method (e.g. 'card', 'klarna') * * @return string */ public static function get_id(): string; /** * Get the keywords for the payment method. These are used by the duplicate detection service. * * @return string[] */ public static function get_keywords(): array; /** * Get the Stripe payment method ID (e.g. 'card_payments', 'klarna_payments') * * @return string */ public static function get_stripe_id(): string; /** * Get the customer-facing title of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_title( ?string $account_country = null ): string; /** * Get the title of the payment method for the settings page. * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_settings_label( ?string $account_country = null ): string; /** * Get the customer-facing description of the payment method * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_description( ?string $account_country = null ): string; /** * Get the list of supported currencies * Empty array means all currencies are supported * * @return string[] Array of currency codes */ public static function get_supported_currencies(): array; /** * Get the list of supported countries * Empty array means all countries are supported * * When account_country is provided, payment methods with domestic transaction * restrictions should return only that country (if supported), enabling * proper filtering at checkout. * * @param string|null $account_country Optional. The merchant's account country. * Some payment methods (e.g. Klarna) have different * supported countries based on the merchant's location. * @return string[] Array of country codes */ public static function get_supported_countries( ?string $account_country = null ): array; /** * Get the payment method capabilities * Examples: tokenization, refunds, capture_later * * @return string[] */ public static function get_capabilities(): array; /** * Get the URL for the payment method's icon * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_icon_url( ?string $account_country = null ): string; /** * Get the URL for the payment method's dark mode icon * * @param string|null $account_country Optional. The merchant's account country. * @return string Returns regular icon URL if no dark mode icon exists */ public static function get_dark_icon_url( ?string $account_country = null ): string; /** * Get the URL for the payment method's settings icon * This icon is used in the payment method settings page. * * @param string|null $account_country Optional. The merchant's account country. * @return string */ public static function get_settings_icon_url( ?string $account_country = null ): string; /** * Get the testing instructions for the payment method * * @param string $account_country The merchant's account country. * @return string HTML string containing testing instructions */ public static function get_testing_instructions( string $account_country ): string; /** * Whether this payment method is available for the given currency and country * * @param string $currency The currency code to check. * @param string $account_country The merchant's account country. * @return bool */ public static function is_available_for( string $currency, string $account_country ): bool; /** * Get the currency limits for the payment method * * @return array<string,array<string,array{min:int,max:int}>> */ public static function get_limits_per_currency(): array; /** * Get minimum amount for a currency and country * * @param string $currency The currency code. * @param string $country The country code. * @return int|null Returns null if no limit is set */ public static function get_minimum_amount( string $currency, string $country ): ?int; /** * Get maximum amount for a currency and country * * @param string $currency The currency code. * @param string $country The country code. * @return int|null Returns null if no limit is set */ public static function get_maximum_amount( string $currency, string $country ): ?int; } includes/multi-currency/SettingsOnboardCta.php 0000777 00000006147 15251156605 0015627 0 ustar 00 <?php /** * WooCommerce Payments Multi-Currency Settings * * @package WooCommerce\Admin */ namespace WCPay\MultiCurrency; use WCPay\MultiCurrency\Interfaces\MultiCurrencyAccountInterface; defined( 'ABSPATH' ) || exit; /** * MultiCurrency settings placeholder containing a CTA to connect the account. */ class SettingsOnboardCta extends \WC_Settings_Page { /** * Link to the Multi-Currency documentation page. * * @var string */ const LEARN_MORE_URL = 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/'; /** * MultiCurrency instance. * * @var MultiCurrency */ private $multi_currency; /** * Instance of MultiCurrencyAccountInterface. * * @var MultiCurrencyAccountInterface */ private $payments_account; /** * Constructor. * * @param MultiCurrency $multi_currency The MultiCurrency instance. * @param MultiCurrencyAccountInterface $payments_account Payments Account instance. */ public function __construct( MultiCurrency $multi_currency, MultiCurrencyAccountInterface $payments_account ) { $this->multi_currency = $multi_currency; $this->payments_account = $payments_account; $this->id = $this->multi_currency->id; $this->label = _x( 'Multi-currency', 'Settings tab label', 'woocommerce-payments' ); parent::__construct(); } /** * Initializes this class' WP hooks. * * @return void */ public function init_hooks() { add_action( 'woocommerce_admin_field_wcpay_currencies_settings_onboarding_cta', [ $this, 'currencies_settings_onboarding_cta' ] ); } /** * Output the call to action button if needing to onboard. */ public function currencies_settings_onboarding_cta() { $href = $this->payments_account->get_provider_onboarding_page_url(); ?> <div> <p> <?php printf( /* translators: %s: WooPayments */ esc_html__( 'To add new currencies to your store, please finish setting up %s.', 'woocommerce-payments' ), 'WooPayments' ); ?> </p> <a href="<?php echo esc_url( $href ); ?>" id="wcpay_enabled_currencies_onboarding_cta" type="button" class="button-primary"> <?php esc_html_e( 'Get started', 'woocommerce-payments' ); ?> </a> </div> <?php } /** * Get settings array. * * @param string $current_section Section being shown. * @return array */ public function get_settings( $current_section = '' ) { // Hide the save button because there are no settings to save. global $hide_save_button; $hide_save_button = true; return [ [ 'title' => __( 'Enabled currencies', 'woocommerce-payments' ), 'desc' => sprintf( /* translators: %s: url to documentation. */ __( 'Accept payments in multiple currencies. Prices are converted based on exchange rates and rounding rules. <a href="%s">Learn more</a>', 'woocommerce-payments' ), self::LEARN_MORE_URL ), 'type' => 'title', 'id' => $this->id . '_enabled_currencies', ], [ 'type' => 'wcpay_currencies_settings_onboarding_cta', ], [ 'type' => 'sectionend', 'id' => $this->id . '_enabled_currencies', ], ]; } } includes/multi-currency/CurrencySwitcherBlock.php 0000777 00000016560 15251156605 0016350 0 ustar 00 <?php /** * WooCommerce Payments Currency Switcher Widget * * @package WooCommerce\Payments */ namespace WCPay\MultiCurrency; use function http_build_query; use function urldecode; defined( 'ABSPATH' ) || exit; /** * Currency Switcher Gutenberg Block. */ class CurrencySwitcherBlock { /** * Instance of Multi-Currency. * * @var MultiCurrency $multi_currency */ protected $multi_currency; /** * Instance of Compatibility. * * @var Compatibility $compatibility */ protected $compatibility; /** * Constructor. * * @param MultiCurrency $multi_currency Instance of Multi-Currency. * @param Compatibility $compatibility Instance of Compatibility. */ public function __construct( MultiCurrency $multi_currency, Compatibility $compatibility ) { $this->multi_currency = $multi_currency; $this->compatibility = $compatibility; } /** * Initializes this class' WP hooks. * * @return void */ public function init_hooks() { add_action( 'init', [ $this, 'init_block_widget' ] ); } /** * Initialize the block currency switcher widget. * * @return void */ public function init_block_widget() { // Automatically load dependencies and version. $this->multi_currency->register_script_with_dependencies( 'woocommerce-payments/multi-currency-switcher', 'dist/multi-currency-switcher-block', [ 'wp-components' ] ); register_block_type( 'woocommerce-payments/multi-currency-switcher', [ 'api_version' => '3', 'editor_script' => 'woocommerce-payments/multi-currency-switcher', 'render_callback' => [ $this, 'render_block_widget' ], 'attributes' => [ 'symbol' => [ 'type' => 'boolean', 'default' => true, ], 'flag' => [ 'type' => 'boolean', 'default' => false, ], 'fontSize' => [ 'type' => 'integer', 'default' => 14, ], 'fontLineHeight' => [ 'type' => 'number', 'default' => 1.5, ], 'fontColor' => [ 'type' => 'string', 'default' => '#000000', ], 'border' => [ 'type' => 'boolean', 'default' => true, ], 'borderRadius' => [ 'type' => 'integer', 'default' => 3, ], 'borderColor' => [ 'type' => 'string', 'default' => '#000000', ], 'backgroundColor' => [ 'type' => 'string', 'default' => 'transparent', ], ], ] ); } /** * Render the content of the block widget. Called by the render_callback callback. * Normally, this could be all done on the JS side, however we need to use a dynamic * block here because the currencies enabled on a site could change, and this would not update * properly on the Gutenberg block, because it is cached. * * @param array $block_attributes The attributes (settings) applicable to this block. We expect this will contain * the widget title, and whether or not we should render both flags and symbols. * * @return string The content to be displayed inside the block widget. */ public function render_block_widget( $block_attributes ): string { if ( $this->compatibility->should_disable_currency_switching() ) { return ''; } $enabled_currencies = $this->multi_currency->get_enabled_currencies(); if ( 1 === count( $enabled_currencies ) ) { return ''; } $with_symbol = $block_attributes['symbol'] ?? true; $with_flag = $block_attributes['flag'] ?? false; $styles = $this->get_widget_styles( $block_attributes ); $div_styles = $this->implode_styles_array( $styles['div'] ); $select_styles = $this->implode_styles_array( $styles['select'] ); $widget_content = '<form>'; $widget_content .= $this->get_get_params(); $widget_content .= '<div class="currency-switcher-holder" style="' . esc_attr( $div_styles ) . '">'; $widget_content .= '<select name="currency" onchange="this.form.submit()" style="' . esc_attr( $select_styles ) . '">'; foreach ( $enabled_currencies as $currency ) { $widget_content .= $this->render_currency_option( $currency, $with_symbol, $with_flag ); } $widget_content .= '</select></div></form>'; // Silence XSS warning because we are manually constructing the content and escaping everything above. // nosemgrep: audit.php.wp.security.xss.block-attr -- reason: we are manually constructing the content and escaping everything above. return $widget_content; } /** * Create an <option> element with provided currency. With symbol and flag if requested. * * @param Currency $currency Currency to use for <option> element. * @param boolean $with_symbol Whether to show the currency symbol. * @param boolean $with_flag Whether to show the currency flag. * * @return string Display HTML of currency <option>, as a string. */ private function render_currency_option( Currency $currency, bool $with_symbol, bool $with_flag ): string { $code = $currency->get_code(); $same_symbol = html_entity_decode( $currency->get_symbol() ) === $code; $text = $code; $selected = $this->multi_currency->get_selected_currency()->code === $code ? 'selected' : ''; if ( $with_symbol && ! $same_symbol ) { $text = $currency->get_symbol() . ' ' . $text; } if ( $with_flag ) { $text = $currency->get_flag() . ' ' . $text; } return '<option value="' . esc_attr( $code ) . '" ' . $selected . '>' . esc_html( $text ) . '</option>'; } /** * Given an array of styling rules, output them as a string containing valid CSS. * * @param array $styles An array of CSS styles. * * @return string */ private function implode_styles_array( array $styles ): string { $return_str = ''; foreach ( $styles as $key => $value ) { $return_str .= $key . ': ' . $value . '; '; } return $return_str; } /** * Generate the styles that need to be applied to the widget based on the block attributes. * * @param array $block_attributes The block attributes. * * @return array */ private function get_widget_styles( array $block_attributes ): array { return [ 'div' => [ 'line-height' => $block_attributes['fontLineHeight'] ?? 1.2, ], 'select' => [ 'padding' => '2px', 'border' => ! empty( $block_attributes['border'] ) ? '1px solid' : '0px solid', 'border-radius' => isset( $block_attributes['borderRadius'] ) ? $block_attributes['borderRadius'] . 'px' : '3px', 'border-color' => $block_attributes['borderColor'] ?? '#000000', 'font-size' => isset( $block_attributes['fontSize'] ) ? $block_attributes['fontSize'] . 'px' : '11px', 'color' => $block_attributes['fontColor'] ?? '#000000', 'background-color' => $block_attributes['backgroundColor'] ?? '#000000', ], ]; } /** * Get hidden inputs for every $_GET param. * This prevents the switcher form to remove them on submit. * * @return string|null */ private function get_get_params() { if ( empty( $_GET ) ) { // phpcs:disable WordPress.Security.NonceVerification return null; } $params = explode( '&', urldecode( http_build_query( $_GET ) ) ); $return = ''; foreach ( $params as $param ) { $name_value = explode( '=', $param ); $name = $name_value[0]; $value = $name_value[1]; if ( 'currency' === $name ) { continue; } $return .= sprintf( '<input type="hidden" name="%s" value="%s" />', esc_attr( $name ), esc_attr( $value ) ); } return $return; } } includes/multi-currency/FrontendPrices.php 0000777 00000030446 15251156605 0015016 0 ustar 00 <?php /** * WooCommerce Payments Multi-Currency Frontend Prices * * @package WooCommerce\Payments */ namespace WCPay\MultiCurrency; use WC_Order; defined( 'ABSPATH' ) || exit; /** * Class that applies Multi-Currency prices on the frontend. */ class FrontendPrices { /** * Compatibility instance. * * @var Compatibility */ protected $compatibility; /** * Multi-Currency instance. * * @var MultiCurrency */ protected $multi_currency; /** * Constructor. * * @param MultiCurrency $multi_currency The MultiCurrency instance. * @param Compatibility $compatibility The Compatibility instance. */ public function __construct( MultiCurrency $multi_currency, Compatibility $compatibility ) { $this->multi_currency = $multi_currency; $this->compatibility = $compatibility; } /** * Initializes this class' WP hooks. * * @return void */ public function init_hooks() { if ( defined( 'DOING_CRON' ) || is_admin() || Utils::is_admin_api_request() ) { return; } // Simple product price hooks. add_filter( 'woocommerce_product_get_price', [ $this, 'get_product_price_string' ], 99, 2 ); add_filter( 'woocommerce_product_get_regular_price', [ $this, 'get_product_price_string' ], 99, 2 ); add_filter( 'woocommerce_product_get_sale_price', [ $this, 'get_product_price_string' ], 99, 2 ); // Variation price hooks. add_filter( 'woocommerce_product_variation_get_price', [ $this, 'get_product_price_string' ], 99, 2 ); add_filter( 'woocommerce_product_variation_get_regular_price', [ $this, 'get_product_price_string' ], 99, 2 ); add_filter( 'woocommerce_product_variation_get_sale_price', [ $this, 'get_product_price_string' ], 99, 2 ); // Variation price range hooks. add_filter( 'woocommerce_variation_prices', [ $this, 'get_variation_price_range' ], 99 ); add_filter( 'woocommerce_get_variation_prices_hash', [ $this, 'add_exchange_rate_to_variation_prices_hash' ], 99 ); // Shipping methods hooks. add_filter( 'woocommerce_shipping_zone_shipping_methods', [ $this, 'convert_free_shipping_method_min_amount' ], 99 ); add_filter( 'woocommerce_shipping_method_add_rate_args', [ $this, 'convert_shipping_method_rate_cost' ], 99 ); // Coupon hooks. add_filter( 'woocommerce_coupon_get_amount', [ $this, 'get_coupon_amount' ], 99, 2 ); add_filter( 'woocommerce_coupon_get_minimum_amount', [ $this, 'get_coupon_min_max_amount' ], 99 ); add_filter( 'woocommerce_coupon_get_maximum_amount', [ $this, 'get_coupon_min_max_amount' ], 99 ); // Order hooks. add_filter( 'woocommerce_new_order', [ $this, 'add_order_meta' ], 99, 2 ); // Price Filter Hooks. add_filter( 'rest_post_dispatch', [ $this, 'maybe_modify_price_ranges_rest_response' ], 10, 3 ); add_filter( 'query_loop_block_query_vars', [ $this, 'maybe_modify_price_ranges_query_var' ], 10, 3 ); } /** * Modifies the price range query parameters when the selected currency is not the same as the store currency. * * This method converts the '_price' parameters based on the selected currency. * * @param array $query The current query variables. * @param \WP_Block $block The current block instance. * @param int $page The current page number. * * @return array The modified query variables. */ public function maybe_modify_price_ranges_query_var( $query, $block, $page ) { if ( 'product' !== $query['post_type'] ) { return $query; } if ( empty( $query['meta_query'] ) || ! is_array( $query['meta_query'] ) ) { return $query; } $store_currency = $this->multi_currency->get_default_currency()->get_code(); $selected_currency = $this->multi_currency->get_selected_currency()->get_code(); // If currencies are the same, no need to convert prices in the query. if ( $store_currency === $selected_currency ) { return $query; } // Traverse and modify the meta_query array. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query $query['meta_query'] = $this->convert_meta_query_price_filters( $query['meta_query'], $store_currency, $selected_currency ); return $query; } /** * Recursively traverses and modifies the meta_query array to adjust '_price' values * from the 'from_currency' to the 'target_currency'. * * @param array $meta_query The meta_query array to traverse. * @param string $from_currency The from currency code. * @param string $target_currency The target currency code. * @param int $depth The current depth of the recursion. * * @return array The modified meta_query array. */ private function convert_meta_query_price_filters( $meta_query, $from_currency, $target_currency, $depth = 0 ) { // Prevent infinite recursion in a malformed meta_query. if ( $depth > 4 ) { return $meta_query; } foreach ( $meta_query as &$mq ) { // If the current element is a nested meta_query with a relation. if ( isset( $mq['relation'] ) && is_array( $mq ) ) { // Recursively modify the nested meta_query. if ( isset( $mq['relation'] ) ) { // Extract the relation and the nested queries. $relation = $mq['relation']; $modified_nested = $this->convert_meta_query_price_filters( $mq, $from_currency, $target_currency, $depth + 1 ); // Reconstruct the meta_query with the modified nested queries. $mq = array_merge( [ 'relation' => $relation ], $modified_nested ); } } elseif ( isset( $mq['key'] ) && '_price' === $mq['key'] && isset( $mq['value'] ) && is_numeric( $mq['value'] ) ) { $converted_price = $this->multi_currency->get_raw_conversion( $mq['value'], $from_currency, $target_currency ); if ( is_numeric( $converted_price ) ) { // Apply floor or ceil based on the 'compare' operator. if ( isset( $mq['compare'] ) ) { if ( '<=' === $mq['compare'] ) { $mq['value'] = (string) ceil( $converted_price ); // max_price. } elseif ( '>=' === $mq['compare'] ) { $mq['value'] = (string) floor( $converted_price ); // min_price. } } } } } unset( $mq ); return $meta_query; } /** * Modify the products/collection-data REST API response to include converted price ranges. * * @param \WP_REST_Response $response The original REST response. * @param \WP_REST_Server $server The REST server instance. * @param \WP_REST_Request $request The REST request instance. * * @return \WP_REST_Response The modified REST response. */ public function maybe_modify_price_ranges_rest_response( $response, $server, $request ) { if ( '/wc/store/v1/products/collection-data' !== $request->get_route() ) { return $response; } $data = $response->get_data(); if ( empty( $data['price_range'] ) || ! is_object( $data['price_range'] ) ) { return $response; } $store_currency = $this->multi_currency->get_default_currency()->get_code(); $selected_currency = $this->multi_currency->get_selected_currency()->get_code(); if ( $store_currency === $selected_currency ) { return $response; } $price_fields = [ 'min_price', 'max_price' ]; foreach ( $price_fields as $field ) { if ( property_exists( $data['price_range'], $field ) && is_numeric( $data['price_range']->$field ) ) { $converted_price = $this->multi_currency->get_price( $data['price_range']->$field, 'product' ); if ( is_numeric( $converted_price ) ) { $data['price_range']->$field = (string) $converted_price; } } } $response->set_data( $data ); return $response; } /** * Returns the price for a product. * * @param mixed $price The product's price. * @param mixed $product WC_Product or null. * * @return mixed The converted product's price. */ public function get_product_price( $price, $product = null ) { if ( ! $price || ! $this->compatibility->should_convert_product_price( $product ) ) { return $price; } return $this->multi_currency->get_price( $price, 'product' ); } /** * Returns the stringified price for a product. * * @param mixed $price The product's price. * @param mixed $product WC_Product or null. * * @return string The converted product's price. */ public function get_product_price_string( $price, $product = null ): string { return (string) $this->get_product_price( $price, $product ); } /** * Returns the price range for a variation. * * @param array $variation_prices The variation's prices. * * @return array The converted variation's prices. */ public function get_variation_price_range( $variation_prices ) { foreach ( $variation_prices as $price_type => $prices ) { foreach ( $prices as $variation_id => $price ) { $variation_prices[ $price_type ][ $variation_id ] = $this->get_product_price_string( $price ); } } return $variation_prices; } /** * Add the exchange rate into account for the variation prices hash. * This is used to recalculate the variation price range when the exchange * rate changes, otherwise the old prices will be cached. * * @param array $prices_hash The variation prices hash. * * @return array The variation prices hash with the current exchange rate. */ public function add_exchange_rate_to_variation_prices_hash( $prices_hash ) { $prices_hash[] = $this->get_product_price( 1 ); return $prices_hash; } /** * Returns the shipping add rate args with cost converted. * * @param array $args Shipping rate args. * * @return array Shipping rate args with converted cost. */ public function convert_shipping_method_rate_cost( $args ) { if ( isset( $args['cost'] ) ) { /** * We need to keep the `cost` structure intact when applying * multi-currency conversions, because downstream it is important * for WooCommerce to keep the taxes flow consistent. */ if ( is_array( $args['cost'] ) ) { $args['cost'] = array_map( function ( $cost ) { return $this->multi_currency->get_price( $cost, 'shipping' ); }, $args['cost'] ); } else { $args['cost'] = $this->multi_currency->get_price( $args['cost'], 'shipping' ); } } return $args; } /** * Returns the amount for a coupon. * * @param mixed $amount The coupon's amount. * @param object $coupon The coupon object. * * @return mixed The converted coupon's amount. */ public function get_coupon_amount( $amount, $coupon ) { $percent_coupon_types = [ 'percent' ]; if ( ! $amount || $coupon->is_type( $percent_coupon_types ) || ! $this->compatibility->should_convert_coupon_amount( $coupon ) ) { return $amount; } return $this->multi_currency->get_price( $amount, 'coupon' ); } /** * Returns the min or max amount for a coupon. * * @param mixed $amount The coupon's min or max amount. * * @return mixed The converted coupon's min or max amount. */ public function get_coupon_min_max_amount( $amount ) { if ( ! $amount ) { return $amount; } // Coupon mix/max prices are treated as products to avoid inconsistencies with charm pricing // making a coupon invalid when the coupon min/max amount is the same as the product's price. return $this->multi_currency->get_price( $amount, 'product' ); } /** * Converts the min_amount of free shipping methods. * * @param array $methods The shipping methods. */ public function convert_free_shipping_method_min_amount( $methods ) { foreach ( $methods as $method ) { // Free shipping min amount is treated as products to avoid inconsistencies with charm pricing // making a method invalid when its min amount is the same as the product's price. if ( 'free_shipping' === $method->id && ! empty( $method->min_amount ) ) { $method->min_amount = $this->multi_currency->get_price( $method->min_amount, 'product' ); } } return $methods; } /** * Adds the exchange rate and default currency to the order's meta if prices have been converted. * * @param int $order_id The order ID. * @param WC_Order $order The order object. */ public function add_order_meta( $order_id, $order ) { $default_currency = $this->multi_currency->get_default_currency(); // Do not add exchange rate if order was made in the store's default currency. if ( $default_currency->get_code() === $order->get_currency() ) { return; } $exchange_rate = $this->multi_currency->get_price( 1, 'exchange_rate' ); $order->update_meta_data( '_wcpay_multi_currency_order_exchange_rate', $exchange_rate ); $order->update_meta_data( '_wcpay_multi_currency_order_default_currency', $default_currency->get_code() ); $order->save_meta_data(); } } includes/multi-currency/Utils.php 0000777 00000007563 15251156605 0013175 0 ustar 00 <?php /** * Class Utils * * @package WooCommerce\Payments\Utils */ namespace WCPay\MultiCurrency; defined( 'ABSPATH' ) || exit; /** * Class that controls Multi-Currency Utils. */ class Utils { /** * Checks backtrace calls to see if a certain call has been made. * * @param array $calls Array of the calls to check for. * * @return bool True if found, false if not. */ public function is_call_in_backtrace( array $calls ): bool { $backtrace = wp_debug_backtrace_summary( null, 0, false ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions foreach ( $calls as $call ) { if ( in_array( $call, $backtrace, true ) ) { return true; } } return false; } /** * Checks the query_vars array for a particular pagename and variable to be set. * * @param array $pages Array of the pagenames to check for. * @param array $vars Array of the vars to check for. * * @return bool True if found, false if not. */ public function is_page_with_vars( array $pages, array $vars ): bool { global $wp; if ( $wp->query_vars && isset( $wp->query_vars['pagename'] ) && in_array( $wp->query_vars['pagename'], $pages, true ) ) { foreach ( $vars as $var ) { if ( isset( $wp->query_vars[ $var ] ) ) { return true; } } } return false; } /** * Checks if is a REST API request and the HTTP referer matches admin url. * * @return boolean */ public static function is_admin_api_request(): bool { return 0 === stripos( wp_get_referer(), admin_url() ) && WC()->is_rest_api_request() && ! self::is_store_api_request(); } /** * Writes the session into the client cookie. * * @param bool $set Should the session cookie be set. * * @return void */ public static function set_customer_session_cookie( bool $set ) { WC()->session->set_customer_session_cookie( $set ); } /** * Returns true if the request is a store REST API request. * * @return bool */ public static function is_store_api_request() { if ( function_exists( 'WC' ) && method_exists( WC(), 'is_store_api_request' ) ) { return WC()->is_store_api_request(); } // The logic below is sourced from `WC()->is_store_api_request()`. if ( empty( $_SERVER['REQUEST_URI'] ) ) { return false; } // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return false !== strpos( $_SERVER['REQUEST_URI'], trailingslashit( rest_get_url_prefix() ) . 'wc/store/' ); } /** * Determine if the request that's currently being processed is a Store API batch request. * * @return bool True if the request is a Store API batch request, false otherwise. */ public static function is_store_batch_request(): bool { // @TODO We should move to a more robust way of getting to the route, like WC is doing in the StoreAPI library. https://github.com/woocommerce/woocommerce/blob/9ac48232a944baa2dbfaa7dd47edf9027cca9519/plugins/woocommerce/src/StoreApi/Authentication.php#L15-L15 if ( isset( $_REQUEST['rest_route'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $rest_route = sanitize_text_field( $_REQUEST['rest_route'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.NonceVerification } else { // Extract the request path from the request URL. $url_parts = wp_parse_url( esc_url_raw( $_SERVER['REQUEST_URI'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $request_path = ! empty( $url_parts['path'] ) ? rtrim( $url_parts['path'], '/' ) : ''; // Remove the REST API prefix from the request path to end up with the route. $rest_route = str_replace( trailingslashit( rest_get_url_prefix() ), '', $request_path ); } // Bail early if the rest route is empty. if ( empty( $rest_route ) ) { return false; } return 1 === preg_match( '@^\/wc\/store(\/v[\d]+)?\/batch@', $rest_route ); } } includes/multi-currency/FrontendCurrencies.php 0000777 00000032703 15251156605 0015671 0 ustar 00 <?php /** * WooCommerce Payments Multi-Currency Frontend Currencies * * @package WooCommerce\Payments */ namespace WCPay\MultiCurrency; use WC_Order; use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface; defined( 'ABSPATH' ) || exit; /** * Class that formats Multi-Currency currencies on the frontend. */ class FrontendCurrencies { /** * Multi-Currency instance. * * @var MultiCurrency */ protected $multi_currency; /** * MultiCurrencyLocalizationInterface instance. * * @var MultiCurrencyLocalizationInterface */ protected $localization_service; /** * Multi-currency utils instance. * * @var Utils */ protected $utils; /** * Multi-currency compatibility instance. * * @var Compatibility */ protected $compatibility; /** * Multi-Currency currency formatting map. * * @var array */ protected $currency_format = []; /** * Order currency code. * * @var string|null */ protected $order_currency; /** * WOO currency cache. * * @var string */ private $woocommerce_currency; /** * Price Decimal Separator cache. * * @var array */ private $price_decimal_separators = []; /** * Selected Currency Code cache. * * @var string */ private $selected_currency_code; /** * Store Currency cache. * * @var Currency */ private $store_currency; /** * Constructor. * * @param MultiCurrency $multi_currency The MultiCurrency instance. * @param MultiCurrencyLocalizationInterface $localization_service The Localization Service instance. * @param Utils $utils Utils instance. * @param Compatibility $compatibility Compatibility instance. */ public function __construct( MultiCurrency $multi_currency, MultiCurrencyLocalizationInterface $localization_service, Utils $utils, Compatibility $compatibility ) { $this->multi_currency = $multi_currency; $this->localization_service = $localization_service; $this->utils = $utils; $this->compatibility = $compatibility; } /** * Initializes this class' WP hooks. * * @return void */ public function init_hooks() { if ( ! is_admin() && ! defined( 'DOING_CRON' ) && ! Utils::is_admin_api_request() ) { // Currency hooks. add_filter( 'woocommerce_currency', [ $this, 'get_woocommerce_currency' ], 900 ); add_filter( 'wc_get_price_decimals', [ $this, 'get_price_decimals' ], 900 ); add_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 900 ); add_filter( 'wc_get_price_thousand_separator', [ $this, 'get_price_thousand_separator' ], 900 ); add_filter( 'woocommerce_price_format', [ $this, 'get_woocommerce_price_format' ], 900 ); add_action( 'before_woocommerce_pay', [ $this, 'init_order_currency_from_query_vars' ] ); add_action( 'woocommerce_order_get_total', [ $this, 'maybe_init_order_currency_from_order_total_prop' ], 900, 2 ); add_action( 'woocommerce_get_formatted_order_total', [ $this, 'maybe_clear_order_currency_after_formatted_order_total' ], 900, 4 ); } add_filter( 'woocommerce_thankyou_order_id', [ $this, 'init_order_currency' ] ); add_action( 'woocommerce_account_view-order_endpoint', [ $this, 'init_order_currency' ], 9 ); add_filter( 'woocommerce_cart_hash', [ $this, 'add_currency_to_cart_hash' ], 900 ); add_filter( 'woocommerce_shipping_method_add_rate_args', [ $this, 'fix_price_decimals_for_shipping_rates' ], 900, 2 ); } /** * The selected currency changed. We discard some cache. * * @return void */ public function selected_currency_changed() { $this->selected_currency_code = null; $this->price_decimal_separators = []; $this->woocommerce_currency = null; $this->store_currency = null; } /** * Removes 'min_price' and 'max_price' from the URL query parameters. * * Clears existing price filters when the currency is changed to prevent inconsistencies. * * @return void */ public function clear_url_price_params() { if ( isset( $_GET['min_price'] ) || isset( $_GET['max_price'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $url = remove_query_arg( [ 'min_price', 'max_price' ] ); wp_safe_redirect( $url ); exit; } } /** * Gets the store currency. * * @return Currency The store currency wrapped as a Currency object */ public function get_store_currency(): Currency { if ( empty( $this->store_currency ) ) { $this->store_currency = $this->multi_currency->get_default_currency(); } return $this->store_currency; } /** * Returns the currency code to be used by WooCommerce. * * @return string The code of the currency to be used. */ public function get_woocommerce_currency(): string { if ( $this->compatibility->should_return_store_currency() ) { return $this->get_store_currency()->get_code(); } if ( empty( $this->woocommerce_currency ) ) { $this->woocommerce_currency = $this->get_selected_currency_code(); } return $this->woocommerce_currency; } /** * Returns the number of decimals to be used by WooCommerce. * * @param int $decimals The original decimal count. * * @return int The number of decimals. */ public function get_price_decimals( $decimals ): int { $currency_code = $this->get_currency_code(); if ( $currency_code !== $this->get_store_currency()->get_code() ) { return absint( $this->localization_service->get_currency_format( $currency_code )['num_decimals'] ); } return $decimals; } /** * Returns the decimal separator to be used by WooCommerce. * * @param string $separator The original separator. * * @return string The decimal separator. */ public function get_price_decimal_separator( $separator ): string { $currency_code = $this->get_currency_code(); $store_currency_code = $this->get_store_currency()->get_code(); if ( $currency_code === $store_currency_code ) { $this->price_decimal_separators[ $currency_code ] = $separator; } if ( empty( $this->price_decimal_separators[ $currency_code ] ) ) { $this->price_decimal_separators[ $currency_code ] = $this->localization_service->get_currency_format( $currency_code )['decimal_sep']; } return $this->price_decimal_separators[ $currency_code ]; } /** * Returns the thousand separator to be used by WooCommerce. * * @param string $separator The original separator. * * @return string The thousand separator. */ public function get_price_thousand_separator( $separator ): string { $currency_code = $this->get_currency_code(); if ( $currency_code !== $this->get_store_currency()->get_code() ) { return $this->localization_service->get_currency_format( $currency_code )['thousand_sep']; } return $separator; } /** * Returns the currency format to be used by WooCommerce. * * @param string $format The original currency format. * * @return string The currency format. */ public function get_woocommerce_price_format( $format ): string { $currency_code = $this->get_currency_code(); if ( $currency_code !== $this->get_store_currency()->get_code() ) { $currency_pos = $this->localization_service->get_currency_format( $currency_code )['currency_pos']; switch ( $currency_pos ) { case 'left': return '%1$s%2$s'; case 'right': return '%2$s%1$s'; case 'left_space': return '%1$s %2$s'; case 'right_space': return '%2$s %1$s'; default: return '%1$s%2$s'; } } return $format; } /** * Adds the currency and exchange rate to the cart hash so it's recalculated properly. * * @param string $hash The cart hash. * * @return string The adjusted cart hash. */ public function add_currency_to_cart_hash( $hash ): string { $currency = $this->multi_currency->get_selected_currency(); return md5( $hash . $currency->get_code() . $currency->get_rate() ); } /** * Inits order currency code. * * @param mixed $arg Either WC_Order or the id of an order are expected, but can be empty. * * @return int|mixed The order id or what was passed as $arg. */ public function init_order_currency( $arg ) { if ( null !== $this->order_currency ) { return $arg; } // We remove these filters here because 'wc_get_order' // can trigger them, leading to an infinitely recursive call. remove_filter( 'woocommerce_price_format', [ $this, 'get_woocommerce_price_format' ], 900 ); remove_filter( 'wc_get_price_thousand_separator', [ $this, 'get_price_thousand_separator' ], 900 ); remove_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 900 ); remove_filter( 'wc_get_price_decimals', [ $this, 'get_price_decimals' ], 900 ); $order = ! $arg instanceof WC_Order ? wc_get_order( $arg ) : $arg; add_filter( 'wc_get_price_decimals', [ $this, 'get_price_decimals' ], 900 ); add_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 900 ); add_filter( 'wc_get_price_thousand_separator', [ $this, 'get_price_thousand_separator' ], 900 ); add_filter( 'woocommerce_price_format', [ $this, 'get_woocommerce_price_format' ], 900 ); if ( $order ) { $this->order_currency = $order->get_currency(); return $order->get_id(); } $this->order_currency = $this->multi_currency->get_selected_currency()->get_code(); return $arg; } /** * Gets the order id from the wp query_vars and then calls init_order_currency. * * @return void */ public function init_order_currency_from_query_vars() { global $wp; if ( ! empty( $wp->query_vars['order-pay'] ) ) { $this->init_order_currency( $wp->query_vars['order-pay'] ); } elseif ( ! empty( $wp->query_vars['order-received'] ) ) { $this->init_order_currency( $wp->query_vars['order-received'] ); } elseif ( ! empty( $wp->query_vars['view-order'] ) ) { $this->init_order_currency( $wp->query_vars['view-order'] ); } } /** * Fixes the decimals for the store currency when shipping rates are being determined. * Our `wc_get_price_decimals` filter returns the decimals for the selected currency during this calculation, which leads to incorrect results. * * @param array $args The argument array to be filtered. * @param object $method The shipping method being calculated. * * @return array */ public function fix_price_decimals_for_shipping_rates( array $args, $method ): array { $args['price_decimals'] = absint( $this->localization_service->get_currency_format( $this->get_store_currency()->get_code() )['num_decimals'] ); return $args; } /** * Returns the current value of order_currency. * * @return ?string The currency code or null. */ public function get_order_currency() { return $this->order_currency; } /** * Maybe init the order_currency when the order total is queried if we should_use_order_currency. * * This works off of filtering during WC_Abstract_Order->get_total, which states it returns a float, however, in the instances of orders with negative * amounts, such as refund orders, it will return a string. * * @param mixed $total The order total. * @param WC_Order $order The order being worked on. * * @return mixed The unmodified total. */ public function maybe_init_order_currency_from_order_total_prop( $total, $order ) { if ( $this->should_use_order_currency() ) { $this->init_order_currency( $order ); } return $total; } /** * If the order_currency is set and we should be using the order currency, clear it. * * This should only be happening on the instances tested for in should_use_order_currency. We would need to clear the order currency once the total * filter is run so that if another total comes up, like in the order list, we use the next order's currency. * * @param string $formatted_total Total to display. * @param WC_Order $order Order data. * @param string $tax_display Type of tax display. * @param bool $display_refunded If should include refunded value. * * @return string The unmodified formatted total. */ public function maybe_clear_order_currency_after_formatted_order_total( $formatted_total, $order, $tax_display, $display_refunded ): string { if ( null !== $this->order_currency && $this->should_use_order_currency() ) { $this->order_currency = null; } return $formatted_total; } /** * Gets the currency code for us to use. * * @return string|null Three letter currency code. */ private function get_currency_code() { if ( $this->should_use_order_currency() ) { $this->init_order_currency_from_query_vars(); return $this->order_currency; } $this->selected_currency_code = $this->get_selected_currency_code(); return $this->selected_currency_code; } /** * Helper function to "cache" the selected currency. * * @return string */ private function get_selected_currency_code(): string { if ( empty( $this->selected_currency_code ) ) { $this->selected_currency_code = $this->multi_currency->get_selected_currency()->get_code(); } return $this->selected_currency_code; } /** * Checks whether currency code used for formatting should be overridden. * * @return bool */ private function should_use_order_currency(): bool { $pages = [ 'my-account', 'checkout' ]; $vars = [ 'order-received', 'order-pay', 'orders', 'view-order' ]; if ( $this->utils->is_page_with_vars( $pages, $vars ) ) { return $this->utils->is_call_in_backtrace( [ 'WC_Shortcode_My_Account::view_order', 'WC_Shortcode_Checkout::order_received', 'WC_Shortcode_Checkout::order_pay', 'WC_Order->get_formatted_order_total', ] ); } return false; } } includes/multi-currency/Settings.php 0000777 00000004205 15251156605 0013663 0 ustar 00 <?php /** * WooCommerce Payments Multi-currency Settings * * @package WooCommerce\Admin */ namespace WCPay\MultiCurrency; defined( 'ABSPATH' ) || exit; /** * Settings. */ class Settings extends \WC_Settings_Page { /** * The id of the plugin. * * @var string */ public $id; /** * The tab label. * * @var string */ public $label; /** * Instance of MultiCurrency class. * * @var MultiCurrency */ protected $multi_currency; /** * Constructor. * * @param MultiCurrency $multi_currency The MultiCurrency instance. */ public function __construct( MultiCurrency $multi_currency ) { $this->multi_currency = $multi_currency; $this->id = $this->multi_currency->id; $this->label = _x( 'Multi-currency', 'Settings tab label', 'woocommerce-payments' ); parent::__construct(); } /** * Initializes this class' WP hooks. * * @return void */ public function init_hooks() { // TODO: Only register emoji script in settings page. Until WC Admin decide if they will enable it too: https://github.com/woocommerce/woocommerce-admin/issues/6388. add_action( 'admin_print_scripts', [ $this, 'maybe_add_print_emoji_detection_script' ] ); add_action( 'woocommerce_admin_field_wcpay_multi_currency_settings_page', [ $this, 'wcpay_multi_currency_settings_page' ] ); } /** * Get settings array. * * @param string $current_section Section being shown. * @return array */ public function get_settings( $current_section = '' ) { return [ [ 'type' => 'wcpay_multi_currency_settings_page', ], ]; } /** * Output container for enabled currencies list. */ public function wcpay_multi_currency_settings_page() { // Hide original save button. $GLOBALS['hide_save_button'] = true; ?> <div id="wcpay_multi_currency_settings_container" aria-describedby="wcpay_multi_currency_settings_container-description"></div> <?php } /** * Load inline Emoji detection script on multi-currency settings page */ public function maybe_add_print_emoji_detection_script() { if ( $this->multi_currency->is_multi_currency_settings_page() ) { print_emoji_detection_script(); } } } includes/multi-currency/CurrencySwitcherWidget.php 0000777 00000011020 15251156605 0016523 0 ustar 00 <?php /** * WooCommerce Payments Currency Switcher Widget * * @package WooCommerce\Payments */ namespace WCPay\MultiCurrency; use WC_Widget; defined( 'ABSPATH' ) || exit; /** * Currency Switcher Widget Class */ class CurrencySwitcherWidget extends WC_Widget { const DEFAULT_SETTINGS = [ 'title' => '', 'symbol' => true, 'flag' => false, ]; /** * Compatibility instance. * * @var Compatibility */ protected $compatibility; /** * Multi-Currency instance. * * @var MultiCurrency */ protected $multi_currency; /** * Register widget with WordPress. * * @param MultiCurrency $multi_currency The MultiCurrency instance. * @param Compatibility $compatibility The Compatibility instance. */ public function __construct( MultiCurrency $multi_currency, Compatibility $compatibility ) { $this->multi_currency = $multi_currency; $this->compatibility = $compatibility; $this->widget_id = 'currency_switcher_widget'; $this->widget_name = __( 'Currency Switcher Widget', 'woocommerce-payments' ); $this->widget_description = __( 'Let your customers switch between your enabled currencies', 'woocommerce-payments' ); $this->settings = [ 'title' => [ 'type' => 'text', 'std' => '', 'label' => __( 'Title', 'woocommerce-payments' ), ], 'symbol' => [ 'type' => 'checkbox', 'std' => true, 'label' => __( 'Display currency symbols', 'woocommerce-payments' ), ], 'flag' => [ 'type' => 'checkbox', 'std' => false, 'label' => __( 'Display flags in supported devices', 'woocommerce-payments' ), ], ]; parent::__construct(); } /** * Front-end display of widget. * * @param array $args Widget arguments. * @param array $instance Saved values from database. */ public function widget( $args, $instance ) { if ( $this->compatibility->should_disable_currency_switching() ) { return; } $enabled_currencies = $this->multi_currency->get_enabled_currencies(); if ( 1 === count( $enabled_currencies ) ) { return; } $instance = wp_parse_args( $instance, self::DEFAULT_SETTINGS ); $title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ); echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput if ( ! empty( $title ) ) { echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput } ?> <form> <?php $this->output_get_params(); ?> <select name="currency" aria-label="<?php echo esc_attr( $title ); ?>" onchange="this.form.submit()" > <?php foreach ( $enabled_currencies as $currency ) { $this->display_currency_option( $currency, $instance['symbol'], $instance['flag'] ); } ?> </select> </form> <?php echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput } /** * Create an <option> element with provided currency. With symbol and flag if requested. * * @param Currency $currency Currency to use for <option> element. * @param boolean $with_symbol Whether to show the currency symbol. * @param boolean $with_flag Whether to show the currency flag. * * @return void Displays HTML of currency <option> */ private function display_currency_option( Currency $currency, bool $with_symbol, bool $with_flag ) { $code = $currency->get_code(); $same_symbol = html_entity_decode( $currency->get_symbol() ) === $code; $text = $code; $selected = $this->multi_currency->get_selected_currency()->code === $code ? ' selected' : ''; if ( $with_symbol && ! $same_symbol ) { $text = $currency->get_symbol() . ' ' . $text; } if ( $with_flag ) { $text = $currency->get_flag() . ' ' . $text; } echo "<option value=\"$code\"$selected>$text</option>"; // phpcs:ignore WordPress.Security.EscapeOutput } /** * Output hidden inputs for every $_GET param. * This prevent the switcher form to remove them on submit. * * @return void */ private function output_get_params() { // phpcs:disable WordPress.Security.NonceVerification if ( empty( $_GET ) ) { return; } $params = explode( '&', urldecode( http_build_query( $_GET ) ) ); foreach ( $params as $param ) { $name_value = explode( '=', $param ); $name = $name_value[0]; $value = $name_value[1]; if ( 'currency' === $name ) { continue; } echo '<input type="hidden" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '" />'; } // phpcs:enable WordPress.Security.NonceVerification } } includes/multi-currency/Tracking.php 0000777 00000015172 15251156605 0013632 0 ustar 00 <?php /** * Class Tracking * * @package WooCommerce\Payments\MultiCurrency */ namespace WCPay\MultiCurrency; defined( 'ABSPATH' ) || exit; /** * Class that controls Multi-Currency Tracking functionality. */ class Tracking { /** * MultiCurrency class. * * @var MultiCurrency */ private $multi_currency; /** * Class constructor. * * @param MultiCurrency $multi_currency MultiCurrency class. */ public function __construct( MultiCurrency $multi_currency ) { $this->multi_currency = $multi_currency; } /** * Initializes this class' WP hooks. * * @return void */ public function init_hooks() { add_filter( 'woocommerce_tracker_data', [ $this, 'add_tracker_data' ], 50 ); } /** * Add our data to the tracking data from WC core. * * @param array $data The array of data WC core has already built. * * @return array Our modified data. */ public function add_tracker_data( array $data ): array { $data[ $this->multi_currency->id ] = [ 'enabled_currencies' => $this->get_enabled_currencies(), 'default_currency' => $this->get_currency_data_array( $this->multi_currency->get_default_currency() ), 'order_counts' => $this->get_mc_order_count(), ]; return $data; } /** * Returns an assoc array of the data we want from a Currency object. * * @param Currency $currency The Currency object we want data from. * * @return array Assoc array with the Currency data. */ private function get_currency_data_array( Currency $currency ): array { $data = [ 'code' => $currency->get_code(), 'name' => html_entity_decode( $currency->get_name() ), ]; // Return early if it's the default currency. if ( $currency->get_code() === $this->multi_currency->get_default_currency()->get_code() ) { return $data; } // Is it a zero decimal currency? $is_zero_decimal = $currency->get_is_zero_decimal(); // Is it using a custom or automatic rate? $rate_type = get_option( $this->multi_currency->id . '_exchange_rate_' . $currency->get_id(), 'automatic' ); $rate_type = 'automatic' === $rate_type ? $rate_type . ' (default)' : $rate_type; // What is the price rounding setting? $price_rounding_default = $is_zero_decimal ? '100' : '1.00'; $price_rounding = $currency->get_rounding(); $price_rounding = $price_rounding_default === $price_rounding ? $price_rounding . ' (default)' : $price_rounding; // What is the price charm setting? $price_charm = $currency->get_charm(); $price_charm = 0.00 === $price_charm ? '0.00 (default)' : $price_charm; $additional_data = [ 'is_zero_decimal' => $is_zero_decimal, 'rate_type' => $rate_type, 'price_rounding' => $price_rounding, 'price_charm' => $price_charm, ]; return array_merge( $data, $additional_data ); } /** * Gets the enabled currencies as an associative array. Excludes the store/default currency. * * @return array Array of currencies, or empty array if none found. */ private function get_enabled_currencies(): array { $enabled_currencies = $this->multi_currency->get_enabled_currencies(); $default_currency = $this->multi_currency->get_default_currency(); unset( $enabled_currencies[ $default_currency->get_code() ] ); $enabled_array = []; foreach ( $enabled_currencies as $currency ) { $enabled_array[ $currency->get_code() ] = $this->get_currency_data_array( $currency ); } return $enabled_array; } /** * Queries the database to see how many orders have been made using Multi-Currency. * * @return array Result count. */ private function get_mc_order_count(): array { global $wpdb; $query_on_orders = " SELECT gateway, currency, SUM(total) AS totals, COUNT(order_id) AS counts FROM ( SELECT orders.id AS order_id, orders.payment_method as gateway, orders.total_amount as total, orders.currency as currency FROM {$wpdb->prefix}wc_orders orders LEFT JOIN {$wpdb->prefix}wc_orders_meta order_meta ON order_meta.order_id = orders.id INNER JOIN {$wpdb->prefix}wc_orders_meta mc_meta ON mc_meta.order_id = orders.id AND mc_meta.meta_key = '_wcpay_multi_currency_order_exchange_rate' WHERE orders.type = 'shop_order' AND orders.status in ( 'wc-completed', 'wc-processing', 'wc-refunded' ) GROUP BY orders.id ) order_gateways GROUP BY currency, gateway "; $query_on_posts = " SELECT gateway, currency, SUM(total) AS totals, COUNT(order_id) AS counts FROM ( SELECT orders.id AS order_id, MAX(CASE WHEN order_meta.meta_key = '_payment_method' THEN order_meta.meta_value END) gateway, MAX(CASE WHEN order_meta.meta_key = '_order_total' THEN order_meta.meta_value END) total, MAX(CASE WHEN order_meta.meta_key = '_order_currency' THEN order_meta.meta_value END) currency FROM {$wpdb->prefix}posts orders LEFT JOIN {$wpdb->prefix}postmeta order_meta ON order_meta.post_id = orders.id INNER JOIN {$wpdb->prefix}postmeta mc_meta ON mc_meta.post_id = orders.id AND mc_meta.meta_key = '_wcpay_multi_currency_order_exchange_rate' WHERE orders.post_type = 'shop_order' AND orders.post_status in ( 'wc-completed', 'wc-processing', 'wc-refunded' ) AND order_meta.meta_key in ( '_payment_method', '_order_total', '_order_currency' ) GROUP BY orders.id ) order_gateways GROUP BY currency, gateway "; if ( class_exists( 'Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) { $orders_by_currency = $wpdb->get_results( $query_on_orders ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } else { $orders_by_currency = $wpdb->get_results( $query_on_posts ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } $currencies = []; $total_count = 0; foreach ( $orders_by_currency as $group ) { // Get current counts and totals. $counts = $currencies[ $group->currency ]['counts'] ?? 0; $totals = $currencies[ $group->currency ]['totals'] ?? 0; // Update the counts and totals for the currency. $currencies[ $group->currency ]['counts'] = $counts + $group->counts; $currencies[ $group->currency ]['totals'] = $totals + $group->totals; // If something provides a 100% discount, the payment method is null. This could be coupons, gift cards, etc. $gateway = $group->gateway ?? 'unknown'; // Update the counts and totals per gateway for the currency. $currencies[ $group->currency ]['gateways'][ $gateway ] = [ 'counts' => $group->counts, 'totals' => $group->totals, ]; // Update the total count. $total_count += $group->counts; } return [ 'counts' => $total_count, 'currencies' => $currencies, ]; } } includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php 0000777 00000014143 15251156605 0021560 0 ustar 00 <?php /** * Class WooCommerceNameYourPrice * * @package WCPay\MultiCurrency\Compatibility */ namespace WCPay\MultiCurrency\Compatibility; use WCPay\MultiCurrency\MultiCurrency; /** * Class that controls Multi Currency Compatibility with WooCommerce Name Your Price Plugin. */ class WooCommerceNameYourPrice extends BaseCompatibility { const NYP_CURRENCY = '_wcpay_multi_currency_nyp_currency'; /** * Init the class. * * @return void */ public function init() { // Add needed actions and filters if Name Your Price is active. if ( class_exists( 'WC_Name_Your_Price' ) ) { // Convert meta prices. add_filter( 'wc_nyp_raw_minimum_price', [ $this, 'get_nyp_prices' ] ); add_filter( 'wc_nyp_raw_maximum_price', [ $this, 'get_nyp_prices' ] ); add_filter( 'wc_nyp_raw_suggested_price', [ $this, 'get_nyp_prices' ] ); // Maybe translate cart prices. add_action( 'woocommerce_add_cart_item_data', [ $this, 'add_initial_currency' ], 20, 3 ); add_filter( 'woocommerce_get_cart_item_from_session', [ $this, 'convert_cart_currency' ], 20, 2 ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 ); // Convert cart editing price. add_filter( 'wc_nyp_edit_in_cart_args', [ $this, 'edit_in_cart_args' ], 10, 2 ); add_filter( 'wc_nyp_get_initial_price', [ $this, 'get_initial_price' ], 10, 3 ); } } /** * Converts the min/max/suggested prices of Name Your Price extension. * * @param mixed $price The price to be filtered. * @return mixed The price as a string or float. */ public function get_nyp_prices( $price ) { return ! $price ? $price : $this->multi_currency->get_price( $price, 'product' ); } /** * Store the inintial currency when item is added. * * @param array $cart_item Extra cart item data being passed to the cart item. * @param int $product_id The id of the product being added to the cart. * @param int $variation_id The id of the variation being added to the cart. * * @return array */ public function add_initial_currency( $cart_item, $product_id, $variation_id ) { $nyp_id = $variation_id ? $variation_id : $product_id; if ( class_exists( '\WC_Name_Your_Price_Helpers' ) && \WC_Name_Your_Price_Helpers::is_nyp( $nyp_id ) && isset( $cart_item['nyp'] ) ) { $currency = $this->multi_currency->get_selected_currency(); $cart_item['nyp_currency'] = $currency->get_code(); $cart_item['nyp_original'] = $cart_item['nyp']; } return $cart_item; } /** * Switch the cart price when currency changes. * Do not convert price if in the same currency as when added to the cart. * This prevevents USD > EUR > USD style conversions and potential rounding problems. * * @param array $cart_item Cart item array. * @param array $values Cart item values e.g. quantity and product_id. * * @return array */ public function convert_cart_currency( $cart_item, $values ) { if ( function_exists( 'WC_Name_Your_Price' ) && isset( $cart_item['nyp_original'] ) && isset( $cart_item['nyp_currency'] ) ) { // Store the original currency in $product meta. $cart_item['data']->update_meta_data( self::NYP_CURRENCY, $cart_item['nyp_currency'] ); $selected_currency = $this->multi_currency->get_selected_currency(); // If the currency is currently the same as at time price entered, restore NYP to original value. if ( $cart_item['nyp_currency'] === $selected_currency->get_code() ) { $cart_item['nyp'] = $cart_item['nyp_original']; } else { $from_currency = $cart_item['nyp_currency']; $raw_price = $cart_item['nyp_original']; $cart_item['nyp'] = $this->multi_currency->get_raw_conversion( $raw_price, $selected_currency->get_code(), $from_currency ); } $cart_item = WC_Name_Your_Price()->cart->set_cart_item( $cart_item ); } return $cart_item; } /** * Checks to see if the product's price should be converted. * * @param bool $return Whether to convert the product's price or not. Default is true. * @param object $product Product object to test. * * @return bool True if it should be converted. */ public function should_convert_product_price( bool $return, $product ): bool { // If it's already false, return it. if ( ! $return ) { return $return; } $currency = $this->multi_currency->get_selected_currency(); // Check for cart items to see if they are in the original currency. if ( $currency->get_code() === $product->get_meta( self::NYP_CURRENCY ) ) { $return = false; } // Check to see if the product is a NYP product. if ( class_exists( '\WC_Name_Your_Price_Helpers' ) && \WC_Name_Your_Price_Helpers::is_nyp( $product ) ) { return false; } return $return; } /** * Add currency to cart edit link. * * @param array $args The cart args. * @param array $cart_item The current cart item. * * @return array */ public function edit_in_cart_args( $args, $cart_item ) { $args['nyp_currency'] = $this->multi_currency->get_selected_currency()->get_code(); return $args; } /** * Maybe convert any prices being edited from the cart * * @param string $initial_price The initial price. * @param mixed $product The product being queried. * @param string $suffix The suffix needed for composites and bundles. * * @return float|string */ public function get_initial_price( $initial_price, $product, $suffix ) { if ( isset( $_REQUEST[ 'nyp_raw' . $suffix ] ) && isset( $_REQUEST['nyp_currency'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $from_currency = wc_clean( wp_unslash( $_REQUEST['nyp_currency'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $raw_price = (float) wc_clean( wp_unslash( $_REQUEST[ 'nyp_raw' . $suffix ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $selected_currency = $this->multi_currency->get_selected_currency(); if ( $from_currency !== $selected_currency->get_code() ) { $initial_price = $this->multi_currency->get_raw_conversion( $raw_price, $selected_currency->get_code(), $from_currency ); } } return $initial_price; } } includes/multi-currency/Compatibility/WooCommerceFedEx.php 0000777 00000004107 15251156605 0020030 0 ustar 00 <?php /** * Class WooCommerceFedEx * * @package WCPay\MultiCurrency\Compatibility */ namespace WCPay\MultiCurrency\Compatibility; use WCPay\MultiCurrency\MultiCurrency; /** * Class that controls Multi Currency Compatibility with WooCommerce FedEx Plugin. */ class WooCommerceFedEx extends BaseCompatibility { /** * Calls to look for in the backtrace when determining whether * to return store currency or skip converting product prices. */ private const WC_SHIPPING_FEDEX_CALLS = [ 'WC_Shipping_Fedex->set_settings', 'WC_Shipping_Fedex->per_item_shipping', 'WC_Shipping_Fedex->box_shipping', 'WC_Shipping_Fedex->get_fedex_api_request', 'WC_Shipping_Fedex->get_fedex_requests', 'WC_Shipping_Fedex->process_result', ]; /** * Init the class. * * @return void */ public function init() { // Add needed actions and filters if FedEx is active. if ( class_exists( 'WC_Shipping_Fedex_Init' ) ) { add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ] ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_return_store_currency', [ $this, 'should_return_store_currency' ] ); } } /** * Checks to see if the product's price should be converted. * * @param bool $return Whether to convert the product's price or not. Default is true. * * @return bool True if it should be converted. */ public function should_convert_product_price( bool $return ): bool { // If it's already false, return it. if ( ! $return ) { return $return; } if ( $this->utils->is_call_in_backtrace( self::WC_SHIPPING_FEDEX_CALLS ) ) { return false; } return $return; } /** * Determine whether to return the store currency or not. * * @param bool $return Whether to return the store currency or not. * * @return bool */ public function should_return_store_currency( bool $return ): bool { // If it's already true, return it. if ( $return ) { return $return; } if ( $this->utils->is_call_in_backtrace( self::WC_SHIPPING_FEDEX_CALLS ) ) { return true; } return $return; } } includes/multi-currency/Compatibility/WooCommercePreOrders.php 0000777 00000001624 15251156605 0020743 0 ustar 00 <?php /** * Class WooCommercePreOrders * * @package WCPay\MultiCurrency\Compatibility */ namespace WCPay\MultiCurrency\Compatibility; use WCPay\MultiCurrency\MultiCurrency; use WCPay\MultiCurrency\Utils; /** * Class that controls Multi Currency Compatibility with WooCommerce Pre-Orders Plugin. */ class WooCommercePreOrders extends BaseCompatibility { /** * Init the class. * * @return void */ public function init() { // Add needed actions and filters if Pre-Orders is active. if ( class_exists( 'WC_Pre_Orders' ) ) { add_filter( 'wc_pre_orders_fee', [ $this, 'wc_pre_orders_fee' ] ); } } /** * Convert the Pre-Orders fee. * * @param array $args Array of args for the Pre-Orders fee. * * @return array */ public function wc_pre_orders_fee( array $args ): array { $args['amount'] = $this->multi_currency->get_price( $args['amount'], 'product' ); return $args; } } includes/multi-currency/Compatibility/WooCommercePointsAndRewards.php 0000777 00000004411 15251156605 0022262 0 ustar 00 <?php /** * Class WooCommercePointsAndRewards * * @package WCPay\MultiCurrency\Compatibility */ namespace WCPay\MultiCurrency\Compatibility; use WCPay\MultiCurrency\Currency; /** * Class that controls Multi Currency Compatibility with WooCommerce Points & Rewards Plugin. */ class WooCommercePointsAndRewards extends BaseCompatibility { /** * Default Currency Code. * * @var string */ private $default_currency_code; /** * Selected Currency Code. * * @var Currency */ private $selected_currency; /** * Init the class. * * @return void */ public function init() { // Add needed filters if Points & Rewards is active and it's not an admin request. if ( is_admin() || ! class_exists( 'WC_Points_Rewards' ) ) { return; } add_filter( 'option_wc_points_rewards_earn_points_ratio', [ $this, 'convert_points_ratio' ], 50 ); add_filter( 'option_wc_points_rewards_redeem_points_ratio', [ $this, 'convert_points_ratio' ], 50 ); } /** * Converts points ratio applying selected currency rate to the monetary value. * * @param string $ratio Store currency points ratio. * @return string Converted points ratio. */ public function convert_points_ratio( string $ratio = '' ): string { // Skip conversion if selected and default currencies are the same. if ( $this->selected_and_default_currency_match() ) { return $ratio; } // Skip conversion on discount to avoid doing it twice. if ( $this->utils->is_call_in_backtrace( [ 'WC_Points_Rewards_Discount->get_discount_data' ] ) ) { return $ratio; } $ratio = explode( ':', $ratio ); $points = (float) ( $ratio[0] ?? 0 ); $value = (float) ( $ratio[1] ?? 0 ); $rate = $this->selected_currency->get_rate(); $value = $value * $rate; return "$points:$value"; } /** * Whether the selected and default currency are the same. * * @return boolean */ private function selected_and_default_currency_match(): bool { if ( empty( $this->default_currency_code ) ) { $this->default_currency_code = $this->multi_currency->get_default_currency()->get_code(); } if ( empty( $this->selected_currency ) ) { $this->selected_currency = $this->multi_currency->get_selected_currency(); } return $this->default_currency_code === $this->selected_currency->get_code(); } } includes/multi-currency/Compatibility/WooCommerceUPS.php 0000777 00000002270 15251156605 0017503 0 ustar 00 <?php /** * Class WooCommerceUPS * * @package WCPay\MultiCurrency\Compatibility */ namespace WCPay\MultiCurrency\Compatibility; use WCPay\MultiCurrency\MultiCurrency; use WCPay\MultiCurrency\Utils; /** * Class that controls Multi Currency Compatibility with WooCommerce UPS Plugin. */ class WooCommerceUPS extends BaseCompatibility { /** * Init the class. * * @return void */ public function init() { // Add needed actions and filters if UPS is active. if ( class_exists( 'WC_Shipping_UPS_Init' ) ) { add_filter( MultiCurrency::FILTER_PREFIX . 'should_return_store_currency', [ $this, 'should_return_store_currency' ] ); } } /** * Determine whether to return the store currency or not. * * @param bool $return Whether to return the store currency or not. * * @return bool */ public function should_return_store_currency( bool $return ): bool { // If it's already true, return it. if ( $return ) { return $return; } $calls = [ 'WC_Shipping_UPS->per_item_shipping', 'WC_Shipping_UPS->box_shipping', 'WC_Shipping_UPS->calculate_shipping', ]; if ( $this->utils->is_call_in_backtrace( $calls ) ) { return true; } return $return; } } includes/multi-currency/Compatibility/BaseCompatibility.php 0000777 00000001561 15251156605 0020302 0 ustar 00 <?php /** * Class BaseCompatibility * * @package WCPay\MultiCurrency\Compatibility */ namespace WCPay\MultiCurrency\Compatibility; use WCPay\MultiCurrency\MultiCurrency; use WCPay\MultiCurrency\Utils; /** * Class that sets up base options for compatibility classes. */ abstract class BaseCompatibility { /** * MultiCurrency class. * * @var MultiCurrency */ protected $multi_currency; /** * Utils class. * * @var Utils */ protected $utils; /** * Constructor. * * @param MultiCurrency $multi_currency MultiCurrency class. * @param Utils $utils Utils class. */ public function __construct( MultiCurrency $multi_currency, Utils $utils ) { $this->multi_currency = $multi_currency; $this->utils = $utils; $this->init(); } /** * Init the class. * * @return void */ abstract public function init(); } includes/multi-currency/Compatibility/WooCommerceBookings.php 0000777 00000015135 15251156605 0020613 0 ustar 00 <?php /** * Class WooCommerceBookings * * @package WCPay\MultiCurrency\Compatibility */ namespace WCPay\MultiCurrency\Compatibility; use WCPay\MultiCurrency\FrontendCurrencies; use WCPay\MultiCurrency\MultiCurrency; use WCPay\MultiCurrency\Utils; use WC_Product; /** * Class that controls Multi Currency Compatibility with WooCommerce Bookings Plugin. */ class WooCommerceBookings extends BaseCompatibility { /** * Front-end currencies. * * @var FrontendCurrencies */ private $frontend_currencies; /** * Constructor. * * @param MultiCurrency $multi_currency MultiCurrency class. * @param Utils $utils Utils class. * @param FrontendCurrencies $frontend_currencies FrontendCurrencies class. */ public function __construct( MultiCurrency $multi_currency, Utils $utils, FrontendCurrencies $frontend_currencies ) { parent::__construct( $multi_currency, $utils ); $this->frontend_currencies = $frontend_currencies; } /** * Init the class. * * @return void */ public function init() { // Add needed actions and filters if Bookings is active. if ( class_exists( 'WC_Bookings' ) ) { if ( ! is_admin() || wp_doing_ajax() ) { add_filter( 'woocommerce_bookings_calculated_booking_cost', [ $this, 'adjust_amount_for_calculated_booking_cost' ], 50, 1 ); add_filter( 'woocommerce_product_get_block_cost', [ $this, 'get_price' ], 50, 1 ); add_filter( 'woocommerce_product_get_cost', [ $this, 'get_price' ], 50, 1 ); add_filter( 'woocommerce_product_get_display_cost', [ $this, 'get_price' ], 50, 1 ); add_filter( 'woocommerce_product_booking_person_type_get_block_cost', [ $this, 'get_price' ], 50, 1 ); add_filter( 'woocommerce_product_booking_person_type_get_cost', [ $this, 'get_price' ], 50, 1 ); add_filter( 'woocommerce_product_get_resource_base_costs', [ $this, 'get_resource_prices' ], 50, 1 ); add_filter( 'woocommerce_product_get_resource_block_costs', [ $this, 'get_resource_prices' ], 50, 1 ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 ); add_filter( 'woocommerce_bookings_process_cost_rules_cost', [ $this, 'get_price' ], 50, 1 ); add_filter( 'woocommerce_bookings_process_cost_rules_base_cost', [ $this, 'get_price' ], 50, 1 ); add_action( 'wp_ajax_wc_bookings_calculate_costs', [ $this, 'add_wc_price_args_filter_for_ajax' ], 9 ); add_action( 'wp_ajax_nopriv_wc_bookings_calculate_costs', [ $this, 'add_wc_price_args_filter_for_ajax' ], 9 ); } } } /** * Adjusts the calculated booking cost for the selected currency, applying rounding and charm pricing as necessary. * * @param mixed $costs The original calculated booking costs. * @return mixed The booking cost adjusted for the selected currency. */ public function adjust_amount_for_calculated_booking_cost( $costs ) { /** * Prevents adjustment of the calculated booking cost during cart addition. * * When a booking is added to the cart, the Booking plugin calculates the booking cost and * overrides the cart item price with this calculated amount. To avoid interfering with this process, * this function skips any additional adjustments at this stage. */ if ( $this->utils->is_call_in_backtrace( [ 'WC_Cart->add_to_cart' ] ) ) { return $costs; } return $this->multi_currency->adjust_amount_for_selected_currency( $costs ); } /** * Retrieves the price for an item, converting it based on the selected currency and context. * * @param mixed $price The item's price. * * @return mixed The converted item's price. */ public function get_price( $price ) { if ( ! $price ) { return $price; } // Skip conversion during specific booking cost calculations to avoid double conversion. if ( $this->utils->is_call_in_backtrace( [ 'WC_Cart->add_to_cart' ] ) && $this->utils->is_call_in_backtrace( [ 'WC_Bookings_Cost_Calculation::calculate_booking_cost' ] ) ) { return $price; } /** * When showing the price in HTML, the function applies currency conversion, charm pricing, * and rounding. For internal calculations, it uses the raw exchange rate, with charm pricing * and rounding adjustments applied only to the final calculated amount (handled in * adjust_amount_for_calculated_booking_cost). */ return $this->multi_currency->get_price( $price, $this->utils->is_call_in_backtrace( [ 'WC_Product_Booking->get_price_html' ] ) ? 'product' : 'exchange_rate' ); } /** * Returns the prices for a resource. * * @param mixed $prices The resource's prices in array format. * * @return mixed The converted resource's prices. */ public function get_resource_prices( $prices ) { if ( is_array( $prices ) ) { foreach ( $prices as $key => $price ) { $prices[ $key ] = $this->get_price( $price ); } } return $prices; } /** * Checks to see if the product's price should be converted. * * @param bool $return Whether to convert the product's price or not. Default is true. * @param WC_Product $product The product instance being checked. * * @return bool True if it should be converted. */ public function should_convert_product_price( bool $return, WC_Product $product ): bool { // If it's already false, or the product is not a booking, ignore it. if ( ! $return || $product->get_type() !== 'booking' ) { return $return; } // Fixes price display on product page and in shop. if ( $this->utils->is_call_in_backtrace( [ 'WC_Product_Booking->get_price_html' ] ) ) { return false; } return $return; } /** * Adds a filter for when there is an ajax call to calculate the booking cost. * * @return void */ public function add_wc_price_args_filter_for_ajax() { add_filter( 'wc_price_args', [ $this, 'filter_wc_price_args' ], 100 ); } /** * Returns the formatting arguments to use when a booking price is calculated on the product. * * @param array $args Original args from wc_price(). * * @return array New arguments matching the selected currency. */ public function filter_wc_price_args( $args ): array { return wp_parse_args( [ 'currency' => $this->multi_currency->get_selected_currency()->get_code(), 'decimal_separator' => $this->frontend_currencies->get_price_decimal_separator( $args['decimal_separator'] ), 'thousand_separator' => $this->frontend_currencies->get_price_thousand_separator( $args['thousand_separator'] ), 'decimals' => $this->frontend_currencies->get_price_decimals( $args['decimals'] ), 'price_format' => $this->frontend_currencies->get_woocommerce_price_format( $args['price_format'] ), ], $args ); } } includes/multi-currency/Compatibility/WooCommerceProductAddOns.php 0000777 00000034341 15251156605 0021551 0 ustar 00 <?php /** * Class WooCommerceProductAddOns * * @package WCPay\MultiCurrency\Compatibility */ namespace WCPay\MultiCurrency\Compatibility; use WC_Product; use WCPay\MultiCurrency\MultiCurrency; use WCPay\MultiCurrency\Utils; /** * Class that controls Multi Currency Compatibility with WooCommerce Product Add Ons Plugin. */ class WooCommerceProductAddOns extends BaseCompatibility { const ADDONS_CONVERTED_META_KEY = '_wcpay_multi_currency_addons_converted'; /** * Init the class. * * @return void */ public function init() { // Add needed actions and filters if Product Add Ons is active. if ( class_exists( 'WC_Product_Addons' ) ) { if ( ! is_admin() && ! defined( 'DOING_CRON' ) ) { add_filter( 'woocommerce_product_addons_option_price_raw', [ $this, 'get_addons_price' ], 50, 2 ); add_filter( 'woocommerce_product_addons_price_raw', [ $this, 'get_addons_price' ], 50, 2 ); add_filter( 'woocommerce_product_addons_params', [ $this, 'product_addons_params' ], 50, 1 ); add_filter( 'woocommerce_product_addons_get_item_data', [ $this, 'get_item_data' ], 50, 3 ); add_filter( 'woocommerce_product_addons_update_product_price', [ $this, 'update_product_price' ], 50, 4 ); add_filter( 'woocommerce_product_addons_order_line_item_meta', [ $this, 'order_line_item_meta' ], 50, 4 ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 ); } if ( wp_doing_ajax() ) { add_filter( 'woocommerce_product_addons_ajax_get_product_price_including_tax', [ $this, 'get_product_calculation_price' ], 50, 3 ); add_filter( 'woocommerce_product_addons_ajax_get_product_price_excluding_tax', [ $this, 'get_product_calculation_price' ], 50, 3 ); } } } /** * Checks to see if the product's price should be converted. * * @param bool $return Whether to convert the product's price or not. Default is true. * @param object $product Product object to test. * * @return bool True if it should be converted. */ public function should_convert_product_price( bool $return, $product ): bool { // If it's already false, return it. if ( ! $return ) { return $return; } // Check for cart items to see if they have already been converted. if ( 1 === $product->get_meta( self::ADDONS_CONVERTED_META_KEY ) ) { return false; } return $return; } /** * Converts the price of an addon from WooCommerce Products Add-on extension. * * @param mixed $price The price to be filtered. * @param array $type The type of the addon. * @return mixed The price as a string or float. */ public function get_addons_price( $price, $type ) { if ( 'percentage_based' === $type['price_type'] ) { // If the addon is a percentage_based type $price is actually a percentage // and doesn't need any conversion. return $price; } return $this->multi_currency->get_price( $price, 'product' ); } /** * Fixes currency formatting issues in Product Add-Ons. PAO gets these values directly from the db options, * so those values aren't filtered. Luckily, there's a filter. * * @param array $params Product Add-Ons global parameters. * * @return array Adjust parameters. */ public function product_addons_params( array $params ): array { $params['currency_format_num_decimals'] = wc_get_price_decimals(); $params['currency_format_decimal_sep'] = wc_get_price_decimal_separator(); $params['currency_format_thousand_sep'] = wc_get_price_thousand_separator(); return $params; } /** * Filters the cart item data meta so we can provide the proper name with converted add on price. * * @param array $addon_data The addon data we are filtering/replacing. * @param array $addon The addon being processed. * @param array $cart_item The cart item being processed. * * @return array */ public function get_item_data( $addon_data, $addon, $cart_item ): array { $price = isset( $cart_item['addons_price_before_calc'] ) ? $cart_item['addons_price_before_calc'] : $addon['price']; $value = $addon['value']; /* * 'woocommerce_addons_add_cart_price_to_value' * * Use this filter to display the price next to each selected add-on option. * By default, add-on prices show up only next to flat fee add-ons. * * @param boolean */ $add_price_to_value = apply_filters( 'woocommerce_addons_add_cart_price_to_value', false, $cart_item ); if ( 0.0 === (float) $addon['price'] ) { $value .= ''; } elseif ( 'percentage_based' === $addon['price_type'] && 0.0 === (float) $price ) { $value .= ''; } elseif ( 'custom_price' === $addon['field_type'] && $addon['price'] ) { if ( class_exists( 'WC_Product_Addons_Helper' ) ) { $addon_price = wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon['price'], $cart_item['data'] ) ); /* translators: %1$s custom addon price in cart */ $value .= sprintf( _x( ' (%1$s)', 'custom price addon price in cart', 'woocommerce-payments' ), $addon_price ); $addon['display'] = $value; } } elseif ( 'flat_fee' === $addon['price_type'] && $addon['price'] ) { if ( class_exists( 'WC_Product_Addons_Helper' ) ) { $addon_price = $this->multi_currency->get_price( $addon['price'], 'product' ); if ( 'input_multiplier' === $addon['field_type'] ) { // Quantity/multiplier add on needs to be split, calculated, then multiplied by input value. $addon_price = $this->multi_currency->get_price( $addon['price'] / $addon['value'], 'product' ) * $addon['value']; } $addon_price = wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon_price, $cart_item['data'] ) ); /* translators: %1$s flat fee addon price in order */ $value .= sprintf( _x( ' (+ %1$s)', 'flat fee addon price in cart', 'woocommerce-payments' ), $addon_price ); } } elseif ( 'quantity_based' === $addon['price_type'] && $addon['price'] && $add_price_to_value ) { if ( class_exists( 'WC_Product_Addons_Helper' ) ) { $addon_price = $this->multi_currency->get_price( $addon['price'], 'product' ); if ( 'input_multiplier' === $addon['field_type'] ) { // Quantity/multiplier add on needs to be split, calculated, then multiplied by input value. $addon_price = $this->multi_currency->get_price( $addon['price'] / $addon['value'], 'product' ) * $addon['value']; } $addon_price = wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon_price, $cart_item['data'] ) ); /* translators: %1$s addon price in order */ $value .= sprintf( _x( ' (%1$s)', 'quantity based addon price in cart', 'woocommerce-payments' ), $addon_price ); } } elseif ( 'percentage_based' === $addon['price_type'] && $addon['price'] && $add_price_to_value ) { // Get the percentage cost in the currency in use, and set the meta data on the product that the value was converted. $_product = wc_get_product( $cart_item['product_id'] ); $price = $this->multi_currency->get_price( $price, 'product' ); $_product->set_price( $price * ( $addon['price'] / 100 ) ); $_product->update_meta_data( self::ADDONS_CONVERTED_META_KEY, 1 ); /* translators: %1$s addon price in order */ $value .= sprintf( _x( ' (%1$s)', 'percentage based addon price in cart', 'woocommerce-payments' ), WC()->cart->get_product_price( $_product ) ); } return [ 'name' => $addon['name'], 'value' => $value, 'display' => isset( $addon['display'] ) ? $addon['display'] : '', ]; } /** * Updates the product price according to converted add on values. * * @param array $updated_prices Prices updated by Product Add-Ons (unused). * @param array $cart_item Cart item meta data. * @param array $prices Original prices passed to Product Add-Ons for calculations. * * @return array */ public function update_product_price( $updated_prices, $cart_item, $prices ): array { $price = $this->multi_currency->get_price( $prices['price'], 'product' ); $regular_price = $this->multi_currency->get_price( $prices['regular_price'], 'product' ); $sale_price = $this->multi_currency->get_price( $prices['sale_price'], 'product' ); $flat_fees = 0; $quantity = $cart_item['quantity']; $price_before_addons = $price; $regular_price_before_addons = $regular_price; $sale_price_before_addons = $sale_price; // TODO: Check compat with Smart Coupons. // Compatibility with Smart Coupons self declared gift amount purchase. $credit_called = ! empty( $_POST['credit_called'] ) ? $_POST['credit_called'] : null; // phpcs:ignore if ( empty( $price ) && ! empty( $credit_called ) ) { // Variable $_POST['credit_called'] is an array. if ( isset( $credit_called[ $cart_item['data']->get_id() ] ) ) { $price = (float) $credit_called[ $cart_item['data']->get_id() ]; $regular_price = $price; $sale_price = $price; } } if ( empty( $price ) && ! empty( $cart_item['credit_amount'] ) ) { $price = (float) $cart_item['credit_amount']; $regular_price = $price; $sale_price = $price; } foreach ( $cart_item['addons'] as $addon ) { // Percentage based and custom defined addon prices do not get converted, all others do. if ( 'percentage_based' === $addon['price_type'] || 'custom_price' === $addon['field_type'] ) { $addon_price = $addon['price']; } elseif ( 'input_multiplier' === $addon['field_type'] ) { // Quantity/multiplier add on needs to be split, calculated, then multiplied by input value. $addon_price = $this->multi_currency->get_price( $addon['price'] / $addon['value'], 'product' ) * $addon['value']; } else { $addon_price = $this->multi_currency->get_price( $addon['price'], 'product' ); } switch ( $addon['price_type'] ) { case 'percentage_based': $price += (float) ( $price_before_addons * ( $addon_price / 100 ) ); $regular_price += (float) ( $regular_price_before_addons * ( $addon_price / 100 ) ); $sale_price += (float) ( $sale_price_before_addons * ( $addon_price / 100 ) ); break; case 'flat_fee': $flat_fee = $quantity > 0 ? (float) ( $addon_price / $quantity ) : 0; $price += $flat_fee; $regular_price += $flat_fee; $sale_price += $flat_fee; $flat_fees += $flat_fee; break; default: $price += (float) $addon_price; $regular_price += (float) $addon_price; $sale_price += (float) $addon_price; break; } } // Let ourselves know this item has had add ons converted. $cart_item['data']->update_meta_data( self::ADDONS_CONVERTED_META_KEY, 1 ); return [ 'price' => $price, 'regular_price' => $regular_price, 'sale_price' => $sale_price, 'addons_flat_fees_sum' => $flat_fees, ]; } /** * Filters the meta data for order line items so that we can properly set values in the names. * * @param array $meta_data A key/value for the meta data to be inserted for the line item. * @param array $addon The addon being processed. * @param \WC_Order_Item_Product $item Order item data. * @param array $values Order item values. * * @return array A key/value for the meta data to be inserted for the line item. */ public function order_line_item_meta( array $meta_data, array $addon, \WC_Order_Item_Product $item, array $values ): array { $add_price_to_value = apply_filters( 'woocommerce_addons_add_order_price_to_value', false, $item ); $value = $addon['value']; // Pass the timestamp as the add-on value in order to save the timestamp to the DB. if ( isset( $addon['timestamp'] ) ) { $value = $addon['timestamp']; } // If there is an add-on price, add the price of the add-on to the label name. if ( $addon['price'] && $add_price_to_value ) { $product = $item->get_product(); if ( 'percentage_based' === $addon['price_type'] && 0.0 !== (float) $product->get_price() ) { // Calculate the percentage price. $addon_price = $product->get_price() * ( $addon['price'] / 100 ); } elseif ( 'custom_price' === $addon['field_type'] ) { // Custom prices do not get converted. $addon_price = $addon['price']; } elseif ( 'input_multiplier' === $addon['field_type'] ) { // Quantity/multiplier add on needs to be split, calculated, then multiplied by input value. $addon_price = $this->multi_currency->get_price( $addon['price'] / $addon['value'], 'product' ) * $addon['value']; } else { // Convert all others. $addon_price = $this->multi_currency->get_price( $addon['price'], 'product' ); } if ( class_exists( 'WC_Product_Addons_Helper' ) ) { $price = html_entity_decode( wp_strip_all_tags( wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon_price, $values['data'] ) ) ), ENT_QUOTES, get_bloginfo( 'charset' ) ); } if ( 'flat_fee' === $addon['price_type'] && $addon['price'] && $add_price_to_value ) { /* translators: %1$s flat fee addon price in order */ $value .= sprintf( _x( ' (+ %1$s)', 'flat fee addon price in order', 'woocommerce-payments' ), $price ); } elseif ( ( 'quantity_based' === $addon['price_type'] || 'percentage_based' === $addon['price_type'] ) && $addon['price'] && $add_price_to_value ) { /* translators: %1$s addon price in order */ $value .= sprintf( _x( ' (%1$s)', 'addon price in order', 'woocommerce-payments' ), $price ); } elseif ( 'custom_price' === $addon['field_type'] ) { /* translators: %1$s custom addon price in order */ $value = sprintf( _x( ' (%1$s)', 'custom addon price in order', 'woocommerce-payments' ), $price ); } $meta_data['raw_price'] = $this->multi_currency->get_price( $addon['price'], 'product' ); } $meta_data['value'] = $value; return $meta_data; } /** * Gets the product price during ajax requests from the product page. * * @param float $price Price to get converted. * @param int $quantity Quantity of the product selected. * @param \WC_Product $product WC_Product related to the price. * * @return float Adjusted price. */ public function get_product_calculation_price( float $price, int $quantity, \WC_Product $product ): float { return $this->multi_currency->get_price( $price / $quantity, 'product' ) * $quantity; } } includes/multi-currency/Compatibility/WooCommerceDeposits.php 0000777 00000012247 15251156605 0020633 0 ustar 00 <?php /** * Class WooCommerceDeposits * * @package WCPay\MultiCurrency\Compatibility */ namespace WCPay\MultiCurrency\Compatibility; use WC_Product; use WCPay\MultiCurrency\MultiCurrency; /** * Class that controls Multi Currency Compatibility with WooCommerce Deposits Plugin. */ class WooCommerceDeposits extends BaseCompatibility { /** * Init the class. * * @return void */ public function init() { if ( class_exists( 'WC_Deposits' ) ) { /* * Multi-currency support was added to WooCommerce Deposits in version 2.0.1. * * This prevents the loading of the compatibility class for Deposits in versions * of Deposits that support multi-currency. * * @see https://github.com/woocommerce/woocommerce-deposits/pull/425 * @see https://github.com/woocommerce/woocommerce-deposits/issues/506 */ if ( version_compare( WC_DEPOSITS_VERSION, '2.0.1', '>=' ) ) { return; } // Add compatibility filters here. add_action( 'woocommerce_deposits_create_order', [ $this, 'modify_order_currency' ] ); add_filter( 'woocommerce_get_cart_contents', [ $this, 'modify_cart_item_deposit_amounts' ] ); add_filter( 'woocommerce_product_get__wc_deposit_amount', [ $this, 'modify_cart_item_deposit_amount_meta' ], 10, 2 ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'maybe_convert_product_prices_for_deposits' ], 10, 2 ); } } /** * Converts the currency for deposit amounts of each cart item, only applies if deposits are enabled on the product. * * @param array $cart_contents The items on the cart. * * @return array */ public function modify_cart_item_deposit_amounts( $cart_contents ) { foreach ( $cart_contents as $cart_item_key => $cart_item ) { if ( ! empty( $cart_item['is_deposit'] ) && isset( $cart_item['deposit_amount'] ) ) { $deposit_amount = (float) $cart_item['deposit_amount']; $cart_contents[ $cart_item_key ]['deposit_amount'] = $this->multi_currency->get_price( $deposit_amount, 'product' ); } } return $cart_contents; } /** * Converts the currency for deposit amounts of each cart item, only applies if deposits are enabled on the product. * * @param float $amount The amount to convert. * @param \WC_Product $product The product to check for. * * @return float */ public function modify_cart_item_deposit_amount_meta( $amount, $product ) { if ( 'percent' === $this->get_product_deposit_type( $product ) && $this->utils->is_call_in_backtrace( [ 'WC_Deposits_Cart_Manager->deposits_form_output' ] ) ) { return $this->multi_currency->get_price( $amount, 'product' ); } return $amount; } /** * Defines if the product prices need to be converted when calculating totals, * if the product's deposit type is a payment plan, then it shouldn't convert it. * * @param bool $result The previous flag for converting the price. * @param \WC_Product $product The product to check for. * * @return bool Whether the price should be converted or not. */ public function maybe_convert_product_prices_for_deposits( $result, $product ) { if ( 'plan' === $this->get_product_deposit_type( $product ) && $this->utils->is_call_in_backtrace( [ 'WC_Cart->calculate_totals' ] ) ) { return false; } return $result; } /** * When creating a new order for the remaining amount, forces the new order currency * to be the same with the deposited order currency. * * @param integer $order_id The created order ID. * * @return void */ public function modify_order_currency( $order_id ) { // We need to get the original order from the first item meta. $order = wc_get_order( $order_id ); $order_items = $order->get_items(); $first_order_item = 0 < ( is_countable( $order_items ) ? count( $order_items ) : 0 ) ? reset( $order_items ) : null; if ( ! $first_order_item ) { return; } // Check if the original order ID is attached to the order item. $original_order_id = wc_get_order_item_meta( $first_order_item->get_id(), '_original_order_id', true ); if ( ! $original_order_id ) { return; } // Check if the original order still exists. $original_order = wc_get_order( $original_order_id ); if ( ! $original_order ) { return; } // Get the new and old order currencies, and match them if unmatched. $saved_currency = $order->get_currency( 'view' ); $original_currency = $original_order->get_currency( 'view' ); if ( $saved_currency !== $original_currency ) { $order->set_currency( $original_currency ); $order->save(); } } /** * Gets the deposit type of a product if deposits are enabled for the product. * * @param \WC_Product $product The product to check. * * @return string|false The product deposit type if deposits are enabled on it, or false. */ private function get_product_deposit_type( $product ) { $product_has_deposit = class_exists( 'WC_Deposits_Product_Manager' ) && call_user_func( [ 'WC_Deposits_Product_Manager', 'deposits_enabled' ], $product ); if ( $product_has_deposit ) { return call_user_func( [ 'WC_Deposits_Product_Manager', 'get_deposit_type' ], $product ); } return false; } } includes/multi-currency/Compatibility/WooCommerceSubscriptions.php 0000777 00000044763 15251156605 0021720 0 ustar 00 <?php /** * Class WooCommerceSubscriptions * * @package WCPay\MultiCurrency\Compatibility */ namespace WCPay\MultiCurrency\Compatibility; use WC_Subscription; use WCPay\MultiCurrency\Logger; use WCPay\MultiCurrency\FrontendCurrencies; use WCPay\MultiCurrency\MultiCurrency; /** * Class that controls Multi Currency Compatibility with WooCommerce Subscriptions Plugin and WCPay Subscriptions. */ class WooCommerceSubscriptions extends BaseCompatibility { /** * Our allowed subscription types. */ const SUBSCRIPTION_TYPES = [ 'renewal', 'resubscribe', 'switch' ]; /** * Subscription switch cart item. * * @var string */ public $switch_cart_item = ''; /** * The current subscription being iterated through on the My Account > Subscriptions page. * * @var WC_Subscription|null */ public $current_my_account_subscription = null; /** * The FrontendCurrencies object. * * @var FrontendCurrencies */ public $frontend_currencies; /** * If we are running through our filters. * * @var bool */ private $running_override_selected_currency_filters = false; /** * Init the class. * * @return void */ public function init() { // Add needed actions and filters if WC Subscriptions or WCPay Subscriptions are active. if ( class_exists( 'WC_Subscriptions' ) || class_exists( 'WC_Payments_Subscriptions' ) ) { if ( ! is_admin() && ! defined( 'DOING_CRON' ) ) { $this->frontend_currencies = $this->multi_currency->get_frontend_currencies(); add_filter( 'woocommerce_subscriptions_product_price', [ $this, 'get_subscription_product_price' ], 50, 2 ); add_filter( 'woocommerce_product_get__subscription_sign_up_fee', [ $this, 'get_subscription_product_signup_fee' ], 50, 2 ); add_filter( 'woocommerce_product_variation_get__subscription_sign_up_fee', [ $this, 'get_subscription_product_signup_fee' ], 50, 2 ); add_filter( 'option_woocommerce_subscriptions_multiple_purchase', [ $this, 'maybe_disable_mixed_cart' ], 50 ); add_filter( MultiCurrency::FILTER_PREFIX . 'override_selected_currency', [ $this, 'override_selected_currency' ], 50 ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_coupon_amount', [ $this, 'should_convert_coupon_amount' ], 50, 2 ); add_filter( MultiCurrency::FILTER_PREFIX . 'should_disable_currency_switching', [ $this, 'should_disable_currency_switching' ], 50 ); add_filter( 'woocommerce_subscription_price_string_details', [ $this, 'maybe_set_current_my_account_subscription' ], 50, 2 ); add_filter( 'woocommerce_get_formatted_subscription_total', [ $this, 'maybe_clear_current_my_account_subscription' ], 50, 2 ); add_filter( 'wc_price', [ $this, 'maybe_get_explicit_format_for_subscription_total' ], 50, 1 ); } } } /** * Converts subscription prices, if needed. * * @param mixed $price The price to be filtered. * @param object $product The product that will have a filtered price. * * @return mixed The price as a string or float. */ public function get_subscription_product_price( $price, $product ) { if ( ! $price || ! $this->should_convert_product_price( true, $product ) ) { return $price; } return $this->multi_currency->get_price( $price, 'product' ); } /** * Converts subscription sign up prices, if needed. * * @param mixed $price The price to be filtered. * @param object $product The product that will have a filtered price. * * @return mixed The price as a string or float. */ public function get_subscription_product_signup_fee( $price, $product ) { if ( ! $price ) { return $price; } $item = $this->get_subscription_type_from_cart( 'switch' ); if ( $item ) { $item_id = ! empty( $item['variation_id'] ) ? $item['variation_id'] : $item['product_id']; $switch_cart_item = $this->switch_cart_item; $this->switch_cart_item = $item['key']; if ( $product->get_id() === $item_id ) { /** * These tests get mildly complex due to, when switching, the sign up fee is queried * several times to determine prorated costs. This means we have to test to see when * the fee actually needs be converted. */ if ( $this->utils->is_call_in_backtrace( [ 'WC_Subscriptions_Cart::set_subscription_prices_for_calculation' ] ) ) { return $price; } // Check to see if it's currently determining prorated prices. if ( $this->utils->is_call_in_backtrace( [ 'WC_Subscriptions_Product::get_sign_up_fee' ] ) && $this->utils->is_call_in_backtrace( [ 'WC_Cart->calculate_totals' ] ) && $item['key'] === $switch_cart_item && ! $this->utils->is_call_in_backtrace( [ 'WCS_Switch_Totals_Calculator->apportion_sign_up_fees' ] ) ) { return $price; } // Check to see if the _subscription_sign_up_fee meta for the product has already been updated. if ( $item['key'] === $switch_cart_item ) { foreach ( $product->get_meta_data() as $meta ) { if ( '_subscription_sign_up_fee' === $meta->get_data()['key'] && ! empty( $meta->get_changes() ) ) { return $price; } } } } } return $this->multi_currency->get_price( $price, 'product' ); } /** * Disables the mixed cart if needed. * * @param string|bool $value Option from the database, or false. * * @return mixed False, yes, or no. */ public function maybe_disable_mixed_cart( $value ) { // If there's a subscription switch in the cart, disable multiple items in the cart. // This is so that subscriptions with different currencies cannot be added to the cart. if ( $this->get_subscription_type_from_cart( 'switch' ) ) { return 'no'; } return $value; } /** * Checks to see if the if the selected currency needs to be overridden. * * The running_override_selected_currency_filters property is used here to avoid infinite loops. * * @param mixed $return Default is false, but could be three letter currency code. * * @return mixed Three letter currency code or false if not. */ public function override_selected_currency( $return ) { // If it's not false, or we are already running filters, exit. if ( $return || $this->running_override_selected_currency_filters ) { return $return; } // If we have a subscription in $current_my_account_subscription, we want to use the currency from that subscription. if ( $this->is_current_my_account_subscription_set() ) { return $this->current_my_account_subscription->get_currency(); } // Loop through subscription types and check for cart items. foreach ( self::SUBSCRIPTION_TYPES as $type ) { $cart_item = $this->get_subscription_type_from_cart( $type ); if ( $cart_item ) { $this->running_override_selected_currency_filters = true; // If we have a cart item, then we can get the order or subscription to pull the currency from. $subscription_type = 'subscription_' . $type; $subscription = $this->get_subscription( $cart_item[ $subscription_type ]['subscription_id'] ); $this->running_override_selected_currency_filters = false; return $subscription ? $subscription->get_currency() : $return; } } // This instance is for when the customer lands on the product page to choose a new subscription tier. $switch_subscription = $this->get_subscription_from_superglobal_switch_id(); return $switch_subscription ? $switch_subscription->get_currency() : $return; } /** * Checks to see if the product's price should be converted. * * @param bool $return Whether to convert the product's price or not. Default is true. * @param object $product Product object to test. * * @return bool True if it should be converted. */ public function should_convert_product_price( bool $return, $product ): bool { // If it's already false, return it. if ( ! $return ) { return $return; } $calls = [ 'WC_Cart_Totals->calculate_item_totals', 'WC_Cart->get_product_subtotal', 'wc_get_price_excluding_tax', 'wc_get_price_including_tax', ]; // Check for renewal. if ( $this->get_subscription_type_from_cart( 'renewal' ) ) { // When WCPay Subs programmatically sets up the cart, we need to return the // converted price so the user lands at the checkout with the correct price. if ( $this->utils->is_call_in_backtrace( [ 'WCS_Cart_Renewal->setup_cart' ] ) ) { return $return; } if ( $this->utils->is_call_in_backtrace( $calls ) ) { return false; } } // Check for resubscribe. if ( $this->get_subscription_type_from_cart( 'resubscribe' ) && $this->utils->is_call_in_backtrace( $calls ) ) { return false; } // WCPay Subs does a check against the product price and the total, we need to return the actual product price for this check. if ( $this->utils->is_call_in_backtrace( [ 'WC_Payments_Subscription_Service->get_recurring_item_data_for_subscription' ] ) && $this->utils->is_call_in_backtrace( [ 'WC_Product->get_price' ] ) ) { return false; } return $return; } /** * Checks to see if the coupon's amount should be converted. * * @param bool $return Whether to convert the coupon's price or not. Default is true. * @param object $coupon Coupon object to test. * * @return bool True if it should be converted. */ public function should_convert_coupon_amount( bool $return, $coupon ): bool { // If it's already false, return it. if ( ! $return ) { return $return; } // We do not need to convert percentage coupons. if ( $this->is_coupon_type( $coupon, 'subscription_percent' ) ) { return false; } // If there's not a renewal in the cart, we can convert. $subscription_renewal = $this->get_subscription_type_from_cart( 'renewal' ); if ( ! $subscription_renewal ) { return true; } /** * We need to allow the early renewal to convert the cost, as it pulls the original value of the coupon. * Subsequent queries for the amount use the first converted amount. * This also works for normal manual renewals. */ if ( ! $this->utils->is_call_in_backtrace( [ 'WCS_Cart_Early_Renewal->setup_cart' ] ) && $this->utils->is_call_in_backtrace( [ 'WC_Discounts->apply_coupon' ] ) && $this->is_coupon_type( $coupon, 'subscription_recurring' ) ) { return false; } return $return; } /** * Checks to see if currency switching should be disabled. * * @param bool $return Whether widgets should be hidden or not. Default is false. * * @return bool */ public function should_disable_currency_switching( bool $return ): bool { // If it's already true, return it. if ( $return ) { return $return; } if ( $this->get_subscription_type_from_cart( 'renewal' ) || $this->get_subscription_type_from_cart( 'resubscribe' ) || $this->get_subscription_type_from_cart( 'switch' ) || $this->get_subscription_from_superglobal_switch_id() ) { return true; } return $return; } /** * Maybe sets the current_my_account_subscription var and clears the FrontendCurrencies cache. * * The my-subscriptions.php template file calls $subscription->get_formatted_order_total(), which then calls $subscription->get_price_string_details(). * At that point in time, if we have certain calls in the backtrace, we need to add the subscription into $current_my_account_subscription so that we * are able to use it later on in the maybe_get_explicit_format_for_subscription_total filter. * * @param array $subscription_details The details related to the subscription. * @param WC_Subscription $subscription The subscription being acted on. * * @return array The unmodified subscription details. */ public function maybe_set_current_my_account_subscription( $subscription_details, $subscription ): array { $calls = [ 'WCS_Template_Loader::get_my_subscriptions ', 'WC_Subscription->get_formatted_order_total', ]; if ( $this->utils->is_call_in_backtrace( $calls ) ) { // If we have our calls in the backtrace, we set our cached sub and clear the FrontendCurrencies cache. $this->current_my_account_subscription = $subscription; $this->frontend_currencies->selected_currency_changed(); } return $subscription_details; } /** * Maybe clears the current_my_account_subscription var and clears the FrontendCurrencies cache. * * During the $subscription->get_formatted_order_total() call we may set the current_my_account_subscription var, and we want to clear it as soon as * we no longer need it. The woocommerce_get_formatted_subscription_total filter is at the end of that call, so we check to see if the var is set, * and if it is, we clear it, and we also clear the FrontendCurrencies cache. * * @param string $formatted The subscription formatted total. * @param WC_Subscription $subscription The subscription being acted on. * * @return string The unmodified subscription formatted total. */ public function maybe_clear_current_my_account_subscription( $formatted, $subscription ): string { if ( $this->is_current_my_account_subscription_set() ) { $this->current_my_account_subscription = null; $this->frontend_currencies->selected_currency_changed(); } return $formatted; } /** * If the current_my_account_subscription var is set, then we use that subscription's currency in order to set the explicit total. * * @param string $html_price Price HTML markup. * * @return string The wc_price with HTML wrapping, possibly with the currency code added for explicit formatting. */ public function maybe_get_explicit_format_for_subscription_total( $html_price ): string { if ( ! $this->is_current_my_account_subscription_set() ) { return $html_price; } if ( ! $this->multi_currency->has_additional_currencies_enabled() ) { return $html_price; } /** * Get the currency code from the subscription, then return the explicit price. */ $currency_code = $this->current_my_account_subscription->get_currency() ?? get_woocommerce_currency(); // This is sourced from WC_Payments_Explicit_Price_Formatter::get_explicit_price_with_currency. $price_to_check = html_entity_decode( wp_strip_all_tags( $html_price ) ); if ( false === strpos( $price_to_check, trim( $currency_code ) ) ) { return $html_price . ' ' . $currency_code; } return $html_price; } /** * Simple check to see if the current_my_account_subscription is a WC_Subscription object. * * @return bool */ private function is_current_my_account_subscription_set(): bool { return is_a( $this->current_my_account_subscription, 'WC_Subscription' ); } /** * Checks the cart values to see if there are subscriptions with specific types present. * * This checks both the cart itself and the session. This is due to there are times when an item may be present in * one place and not the other. We need to make sure that if an item is in either we are not creating double conversions. * * @param string $type The type of subscription to look for in the cart. * * @return mixed False if none found, or the subscription cart item as an array. */ private function get_subscription_type_from_cart( $type ) { // Make sure we're looking for allowed types. if ( ! in_array( $type, self::SUBSCRIPTION_TYPES, true ) ) { return false; } // Set the sub type cart key. $subscription_type = 'subscription_' . $type; // Go through each cart item and if it matches the type, return that item. if ( isset( WC()->cart ) && is_array( WC()->cart->cart_contents ) && ! empty( WC()->cart->cart_contents ) ) { foreach ( WC()->cart->cart_contents as $cart_item ) { if ( isset( $cart_item[ $subscription_type ] ) ) { return $cart_item; } } } // Go through each session cart item and if it matches the type, return that item. if ( isset( WC()->session ) && is_array( WC()->session->get( 'cart' ) ) && ! empty( WC()->session->get( 'cart' ) ) ) { foreach ( WC()->session->get( 'cart' ) as $cart_item ) { if ( isset( $cart_item[ $subscription_type ] ) ) { return $cart_item; } } } return false; } /** * Getter for subscription objects. * * @param mixed $the_subscription Post object or post ID of the order. * @return WC_Subscription|bool The subscription object, or false if it cannot be found. */ private function get_subscription( $the_subscription ) { if ( ! function_exists( 'wcs_get_subscription' ) ) { return false; } return wcs_get_subscription( $the_subscription ); } /** * Checks $_GET superglobal for a switch ID from the `switch-subscription` param if it exists. * This `switch-subscription` param is added to the URL when a customer * has initiated a switch from the My Account → Subscription page. * * @return WC_Subscription|bool The subscription object, or false if it cannot be found. */ private function get_subscription_from_superglobal_switch_id() { // Return false if there's no nonce, or if it fails. if ( ! isset( $_GET['_wcsnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wcsnonce'] ), 'wcs_switch_request' ) ) { return false; } // Return false if the param isn't set, or if it isn't numeric. if ( ! isset( $_GET['switch-subscription'] ) || ! is_numeric( $_GET['switch-subscription'] ) ) { return false; } // Get the switch ID from the param. $switch_id = (int) sanitize_key( $_GET['switch-subscription'] ); // Get the sub, preventing an infinite loop with running_override_selected_currency_filters. $this->running_override_selected_currency_filters = true; $switch_subscription = $this->get_subscription( $switch_id ); $this->running_override_selected_currency_filters = false; // Confirm the sub user matches current user, and return the sub. if ( $switch_subscription && $switch_subscription->get_customer_id() === get_current_user_id() ) { return $switch_subscription; } else { Logger::notice( 'User (' . get_current_user_id() . ') attempted to switch a subscription (' . $switch_subscription->get_id() . ') not assigned to them.' ); } return false; } /** * Checks to see if the coupon passed is of a specified type. * * @param \WC_Coupon $coupon Coupon to test. * @param string $type Type of coupon to test for. * * @return bool True on match. */ private function is_coupon_type( $coupon, string $type ) { $types = null; switch ( $type ) { case 'subscription_percent': $types = [ 'recurring_percent', 'sign_up_fee_percent', 'renewal_percent' ]; break; case 'subscription_recurring': $types = [ 'recurring_fee', 'recurring_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart' ]; break; } if ( in_array( $coupon->get_discount_type(), $types, true ) ) { return true; } return false; } } includes/multi-currency/CountryFlags.php 0000777 00000014516 15251156605 0014511 0 ustar 00 <?php /** * Class CountryFlags * * @package WooCommerce\Payments */ namespace WCPay\MultiCurrency; defined( 'ABSPATH' ) || exit; /** * Class that bring flags per country/currency. */ class CountryFlags { const EMOJI_COUNTRIES_FLAGS = [ 'AD' => '🇦🇩', 'AE' => '🇦🇪', 'AF' => '🇦🇫', 'AG' => '🇦🇬', 'AI' => '🇦🇮', 'AL' => '🇦🇱', 'AM' => '🇦🇲', 'AO' => '🇦🇴', 'AQ' => '🇦🇶', 'AR' => '🇦🇷', 'AS' => '🇦🇸', 'AT' => '🇦🇹', 'AU' => '🇦🇺', 'AW' => '🇦🇼', 'AX' => '🇦🇽', 'AZ' => '🇦🇿', 'BA' => '🇧🇦', 'BB' => '🇧🇧', 'BD' => '🇧🇩', 'BE' => '🇧🇪', 'BF' => '🇧🇫', 'BG' => '🇧🇬', 'BH' => '🇧🇭', 'BI' => '🇧🇮', 'BJ' => '🇧🇯', 'BL' => '🇧🇱', 'BM' => '🇧🇲', 'BN' => '🇧🇳', 'BO' => '🇧🇴', 'BQ' => '🇧🇶', 'BR' => '🇧🇷', 'BS' => '🇧🇸', 'BT' => '🇧🇹', 'BV' => '🇧🇻', 'BW' => '🇧🇼', 'BY' => '🇧🇾', 'BZ' => '🇧🇿', 'CA' => '🇨🇦', 'CC' => '🇨🇨', 'CD' => '🇨🇩', 'CF' => '🇨🇫', 'CG' => '🇨🇬', 'CH' => '🇨🇭', 'CI' => '🇨🇮', 'CK' => '🇨🇰', 'CL' => '🇨🇱', 'CM' => '🇨🇲', 'CN' => '🇨🇳', 'CO' => '🇨🇴', 'CR' => '🇨🇷', 'CU' => '🇨🇺', 'CV' => '🇨🇻', 'CW' => '🇨🇼', 'CX' => '🇨🇽', 'CY' => '🇨🇾', 'CZ' => '🇨🇿', 'DE' => '🇩🇪', 'DJ' => '🇩🇯', 'DK' => '🇩🇰', 'DM' => '🇩🇲', 'DO' => '🇩🇴', 'DZ' => '🇩🇿', 'EC' => '🇪🇨', 'EE' => '🇪🇪', 'EG' => '🇪🇬', 'EH' => '🇪🇭', 'ER' => '🇪🇷', 'ES' => '🇪🇸', 'ET' => '🇪🇹', 'EU' => '🇪🇺', 'FI' => '🇫🇮', 'FJ' => '🇫🇯', 'FK' => '🇫🇰', 'FM' => '🇫🇲', 'FO' => '🇫🇴', 'FR' => '🇫🇷', 'GA' => '🇬🇦', 'GB' => '🇬🇧', 'GD' => '🇬🇩', 'GE' => '🇬🇪', 'GF' => '🇬🇫', 'GG' => '🇬🇬', 'GH' => '🇬🇭', 'GI' => '🇬🇮', 'GL' => '🇬🇱', 'GM' => '🇬🇲', 'GN' => '🇬🇳', 'GP' => '🇬🇵', 'GQ' => '🇬🇶', 'GR' => '🇬🇷', 'GS' => '🇬🇸', 'GT' => '🇬🇹', 'GU' => '🇬🇺', 'GW' => '🇬🇼', 'GY' => '🇬🇾', 'HK' => '🇭🇰', 'HM' => '🇭🇲', 'HN' => '🇭🇳', 'HR' => '🇭🇷', 'HT' => '🇭🇹', 'HU' => '🇭🇺', 'ID' => '🇮🇩', 'IE' => '🇮🇪', 'IL' => '🇮🇱', 'IM' => '🇮🇲', 'IN' => '🇮🇳', 'IO' => '🇮🇴', 'IQ' => '🇮🇶', 'IR' => '🇮🇷', 'IS' => '🇮🇸', 'IT' => '🇮🇹', 'JE' => '🇯🇪', 'JM' => '🇯🇲', 'JO' => '🇯🇴', 'JP' => '🇯🇵', 'KE' => '🇰🇪', 'KG' => '🇰🇬', 'KH' => '🇰🇭', 'KI' => '🇰🇮', 'KM' => '🇰🇲', 'KN' => '🇰🇳', 'KP' => '🇰🇵', 'KR' => '🇰🇷', 'KW' => '🇰🇼', 'KY' => '🇰🇾', 'KZ' => '🇰🇿', 'LA' => '🇱🇦', 'LB' => '🇱🇧', 'LC' => '🇱🇨', 'LI' => '🇱🇮', 'LK' => '🇱🇰', 'LR' => '🇱🇷', 'LS' => '🇱🇸', 'LT' => '🇱🇹', 'LU' => '🇱🇺', 'LV' => '🇱🇻', 'LY' => '🇱🇾', 'MA' => '🇲🇦', 'MC' => '🇲🇨', 'MD' => '🇲🇩', 'ME' => '🇲🇪', 'MF' => '🇲🇫', 'MG' => '🇲🇬', 'MH' => '🇲🇭', 'MK' => '🇲🇰', 'ML' => '🇲🇱', 'MM' => '🇲🇲', 'MN' => '🇲🇳', 'MO' => '🇲🇴', 'MP' => '🇲🇵', 'MQ' => '🇲🇶', 'MR' => '🇲🇷', 'MS' => '🇲🇸', 'MT' => '🇲🇹', 'MU' => '🇲🇺', 'MV' => '🇲🇻', 'MW' => '🇲🇼', 'MX' => '🇲🇽', 'MY' => '🇲🇾', 'MZ' => '🇲🇿', 'NA' => '🇳🇦', 'NC' => '🇳🇨', 'NE' => '🇳🇪', 'NF' => '🇳🇫', 'NG' => '🇳🇬', 'NI' => '🇳🇮', 'NL' => '🇳🇱', 'NO' => '🇳🇴', 'NP' => '🇳🇵', 'NR' => '🇳🇷', 'NU' => '🇳🇺', 'NZ' => '🇳🇿', 'OM' => '🇴🇲', 'PA' => '🇵🇦', 'PE' => '🇵🇪', 'PF' => '🇵🇫', 'PG' => '🇵🇬', 'PH' => '🇵🇭', 'PK' => '🇵🇰', 'PL' => '🇵🇱', 'PM' => '🇵🇲', 'PN' => '🇵🇳', 'PR' => '🇵🇷', 'PS' => '🇵🇸', 'PT' => '🇵🇹', 'PW' => '🇵🇼', 'PY' => '🇵🇾', 'QA' => '🇶🇦', 'RE' => '🇷🇪', 'RO' => '🇷🇴', 'RS' => '🇷🇸', 'RU' => '🇷🇺', 'RW' => '🇷🇼', 'SA' => '🇸🇦', 'SB' => '🇸🇧', 'SC' => '🇸🇨', 'SD' => '🇸🇩', 'SE' => '🇸🇪', 'SG' => '🇸🇬', 'SH' => '🇸🇭', 'SI' => '🇸🇮', 'SJ' => '🇸🇯', 'SK' => '🇸🇰', 'SL' => '🇸🇱', 'SM' => '🇸🇲', 'SN' => '🇸🇳', 'SO' => '🇸🇴', 'SR' => '🇸🇷', 'SS' => '🇸🇸', 'ST' => '🇸🇹', 'SV' => '🇸🇻', 'SX' => '🇸🇽', 'SY' => '🇸🇾', 'SZ' => '🇸🇿', 'TC' => '🇹🇨', 'TD' => '🇹🇩', 'TF' => '🇹🇫', 'TG' => '🇹🇬', 'TH' => '🇹🇭', 'TJ' => '🇹🇯', 'TK' => '🇹🇰', 'TL' => '🇹🇱', 'TM' => '🇹🇲', 'TN' => '🇹🇳', 'TO' => '🇹🇴', 'TR' => '🇹🇷', 'TT' => '🇹🇹', 'TV' => '🇹🇻', 'TW' => '🇹🇼', 'TZ' => '🇹🇿', 'UA' => '🇺🇦', 'UG' => '🇺🇬', 'UM' => '🇺🇲', 'US' => '🇺🇸', 'UY' => '🇺🇾', 'UZ' => '🇺🇿', 'VA' => '🇻🇦', 'VC' => '🇻🇨', 'VE' => '🇻🇪', 'VG' => '🇻🇬', 'VI' => '🇻🇮', 'VN' => '🇻🇳', 'VU' => '🇻🇺', 'WF' => '🇼🇫', 'WS' => '🇼🇸', 'XK' => '🇽🇰', 'YE' => '🇾🇪', 'YT' => '🇾🇹', 'ZA' => '🇿🇦', 'ZM' => '🇿🇲', 'ZW' => '🇿🇼', ]; /** * Retrieves a flag by country code. * * @param string $country country alpha-2 code (ISO 3166) like US. * @return string */ public static function get_by_country( string $country ): string { return self::EMOJI_COUNTRIES_FLAGS[ $country ] ?? ''; } /** * Retrieves a flag by currency code. * * @param string $currency currency code (ISO 4217) like USD. * @return string */ public static function get_by_currency( string $currency ): string { $exceptions = [ 'ANG' => '', 'BTC' => '', 'XAF' => '', 'XCD' => '', 'XOF' => '', 'XPF' => '', ]; $flag = $exceptions[ $currency ] ?? self::get_by_country( substr( $currency, 0, -1 ) ); return $flag; } } includes/multi-currency/Exceptions/InvalidCurrencyException.php 0000777 00000000456 15251156605 0021170 0 ustar 00 <?php /** * Class InvalidCurrencyException * * @package WooCommerce\Payments */ namespace WCPay\MultiCurrency\Exceptions; use Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing errors when an invalid currency is used. */ class InvalidCurrencyException extends Exception {} includes/multi-currency/Exceptions/InvalidCurrencyRateException.php 0000777 00000000473 15251156605 0022003 0 ustar 00 <?php /** * Class InvalidCurrencyRateException * * @package WooCommerce\Payments */ namespace WCPay\MultiCurrency\Exceptions; use Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing errors when an invalid currency rate is used. */ class InvalidCurrencyRateException extends Exception {} includes/multi-currency/Compatibility.php 0000777 00000017262 15251156605 0014703 0 ustar 00 <?php /** * Class Compatibility * * @package WooCommerce\Payments\Compatibility */ namespace WCPay\MultiCurrency; use WC_Order; use WC_Order_Refund; use WCPay\MultiCurrency\Compatibility\BaseCompatibility; use WCPay\MultiCurrency\Compatibility\WooCommerceBookings; use WCPay\MultiCurrency\Compatibility\WooCommerceFedEx; use WCPay\MultiCurrency\Compatibility\WooCommerceNameYourPrice; use WCPay\MultiCurrency\Compatibility\WooCommercePreOrders; use WCPay\MultiCurrency\Compatibility\WooCommerceProductAddOns; use WCPay\MultiCurrency\Compatibility\WooCommerceSubscriptions; use WCPay\MultiCurrency\Compatibility\WooCommerceUPS; use WCPay\MultiCurrency\Compatibility\WooCommerceDeposits; use WCPay\MultiCurrency\Compatibility\WooCommercePointsAndRewards; defined( 'ABSPATH' ) || exit; /** * Class that controls Multi-Currency Compatibility. */ class Compatibility extends BaseCompatibility { /** * Compatibility classes. * * @var array */ protected $compatibility_classes = []; /** * Init the class. * * @return void */ public function init() { add_action( 'init', [ $this, 'init_compatibility_classes' ], 11 ); if ( defined( 'DOING_CRON' ) ) { add_filter( 'woocommerce_admin_sales_record_milestone_enabled', [ $this, 'attach_order_modifier' ] ); } } /** * Initializes our compatibility classes. * * @return void */ public function init_compatibility_classes() { if ( 1 < count( $this->multi_currency->get_enabled_currencies() ) ) { $this->compatibility_classes[] = new WooCommerceBookings( $this->multi_currency, $this->utils, $this->multi_currency->get_frontend_currencies() ); $this->compatibility_classes[] = new WooCommerceFedEx( $this->multi_currency, $this->utils ); $this->compatibility_classes[] = new WooCommerceNameYourPrice( $this->multi_currency, $this->utils ); $this->compatibility_classes[] = new WooCommercePreOrders( $this->multi_currency, $this->utils ); $this->compatibility_classes[] = new WooCommerceProductAddOns( $this->multi_currency, $this->utils ); $this->compatibility_classes[] = new WooCommerceSubscriptions( $this->multi_currency, $this->utils ); $this->compatibility_classes[] = new WooCommerceUPS( $this->multi_currency, $this->utils ); $this->compatibility_classes[] = new WooCommerceDeposits( $this->multi_currency, $this->utils ); $this->compatibility_classes[] = new WooCommercePointsAndRewards( $this->multi_currency, $this->utils ); } } /** * Returns the compatibility classes. * * @return array */ public function get_compatibility_classes(): array { return $this->compatibility_classes; } /** * Checks to see if the selected currency needs to be overridden. * * @return mixed Three letter currency code or false if not. */ public function override_selected_currency() { return apply_filters( MultiCurrency::FILTER_PREFIX . 'override_selected_currency', false ); } /** * Deprecated method, please use should_disable_currency_switching. * * @return bool False if it shouldn't be hidden, true if it should. */ public function should_hide_widgets(): bool { wc_deprecated_function( __FUNCTION__, '6.5.0', 'Compatibility::should_disable_currency_switching' ); return $this->should_disable_currency_switching(); } /** * Checks to see if currency switching should be disabled, such as the widgets and the automatic geolocation switching. * * @return bool False if no, true if yes. */ public function should_disable_currency_switching(): bool { $return = false; /** * If the pay_for_order parameter is set, we disable currency switching. * * WooCommerce itself handles all the heavy lifting and verification on the Order Pay page, we just need to * make sure the currency switchers are not displayed. This is due to once the order is created, the currency * itself should remain static. */ if ( isset( $_GET['pay_for_order'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $return = true; } // If someone has hooked into the deprecated filter, throw a notice and then apply the filtering. if ( has_action( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets' ) ) { wc_deprecated_hook( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets', '6.5.0', MultiCurrency::FILTER_PREFIX . 'should_disable_currency_switching' ); $return = apply_filters( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets', $return ); } return apply_filters( MultiCurrency::FILTER_PREFIX . 'should_disable_currency_switching', $return ); } /** * Checks to see if the coupon's amount should be converted. * * @param object $coupon Coupon object to test. * * @return bool True if it should be converted. */ public function should_convert_coupon_amount( $coupon = null ): bool { if ( ! $coupon ) { return true; } return apply_filters( MultiCurrency::FILTER_PREFIX . 'should_convert_coupon_amount', true, $coupon ); } /** * Checks to see if the product's price should be converted. * * @param object $product Product object to test. * * @return bool True if it should be converted. */ public function should_convert_product_price( $product = null ): bool { if ( ! $product ) { return true; } return apply_filters( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', true, $product ); } /** * Determines if the store currency should be returned or not. * * @return bool */ public function should_return_store_currency(): bool { return apply_filters( MultiCurrency::FILTER_PREFIX . 'should_return_store_currency', false ); } /** * This filter is called when the best sales day logic is called. We use it to add another filter which will * convert the order prices used in this inbox notification. * * @param bool $arg Whether or not the best sales day logic should execute. We will just return this as is to * respect the existing behaviour. * * @return bool */ public function attach_order_modifier( $arg ) { // Attach our filter to modify the order prices. add_filter( 'woocommerce_order_query', [ $this, 'convert_order_prices' ] ); // This will be a bool value indication whether the best day logic should be run. Let's just return it as is. return $arg; } /** * When a request is made by the "Best Sales Day" Inbox notification, we want to hook into this and convert * the order totals to the store default currency. * * @param WC_Order[]|WC_Order_Refund[] $results The results returned by the orders query. * * @return array|object of WC_Order objects */ public function convert_order_prices( $results ) { $backtrace_calls = [ 'Automattic\WooCommerce\Admin\Notes\NewSalesRecord::sum_sales_for_date', 'Automattic\WooCommerce\Admin\Notes\NewSalesRecord::possibly_add_note', ]; // If the results are not an array, or if the call we're expecting isn't in the backtrace, then just do nothing and return the results. if ( ! is_array( $results ) || ! $this->utils->is_call_in_backtrace( $backtrace_calls ) ) { return $results; } $default_currency = $this->multi_currency->get_default_currency(); if ( ! $default_currency ) { return $results; } foreach ( $results as $order ) { if ( ! $order || $order->get_currency() === $default_currency->get_code() || ! $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true ) || $order->get_meta( '_wcpay_multi_currency_order_default_currency', true ) !== $default_currency->get_code() ) { continue; } $exchange_rate = $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true ); $order->set_total( wc_format_decimal( $order->get_total() * ( 1 / $exchange_rate ), wc_get_price_decimals() ) ); } remove_filter( 'woocommerce_order_query', [ $this, 'convert_order_prices' ] ); return $results; } } includes/multi-currency/Currency.php 0000777 00000014176 15251156605 0013665 0 ustar 00 <?php /** * Class Currency * * @package WooCommerce\Payments */ namespace WCPay\MultiCurrency; use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface; defined( 'ABSPATH' ) || exit; /** * Multi-Currency Currency object. */ class Currency implements \JsonSerializable { /** * Three letter currency code. * * @var string */ public $code; /** * Currency conversion rate. * * @var float */ public $rate; /** * Currency charm rate after conversion and rounding. * * @var float|null */ private $charm; /** * Is currency default for store? * * @var bool */ private $is_default = false; /** * Currency rounding rate after conversion. * * @var string|null */ private $rounding; /** * Is currency zero decimal? * * @var bool */ private $is_zero_decimal = false; /** * A timestamp representing the time this currency was last fetched successfully from the server. * * @var int|null */ private $last_updated; /** * Instance of MultiCurrencyLocalizationInterface. * * @var MultiCurrencyLocalizationInterface */ private $localization_service; /** * Constructor. * * @param MultiCurrencyLocalizationInterface $localization_service Localization service instance. * @param string $code Three letter currency code. * @param float $rate The conversion rate. * @param int|null $last_updated The time this currency was last updated. */ public function __construct( MultiCurrencyLocalizationInterface $localization_service, $code = '', float $rate = 1.0, $last_updated = null ) { $this->localization_service = $localization_service; $this->code = $code; $this->rate = $rate; if ( get_woocommerce_currency() === $code ) { $this->is_default = true; } // Set zero-decimal style based on WC locale information. $this->is_zero_decimal = 0 === $this->localization_service->get_currency_format( $code )['num_decimals']; if ( ! is_null( $last_updated ) ) { $this->last_updated = $last_updated; } } /** * Retrieves the currency's translated name from WooCommerce core. * * @param string $code The currency code. */ public function get_currency_name_from_code( $code ): string { $wc_currencies = get_woocommerce_currencies(); return $wc_currencies[ $code ]; } /** * Retrieves the currency's code. * * @return string Three letter currency code. */ public function get_code(): string { return $this->code; } /** * Retrieves the currency's charm rate. * * @return float Charm rate. */ public function get_charm(): float { return is_null( $this->charm ) ? 0.00 : $this->charm; } /** * Retrieves the currency's flag. * * @return string Currency flag. */ public function get_flag(): string { // Maybe add param img/emoji to return which you want? return CountryFlags::get_by_currency( $this->code ); } /** * Retrieves the currency code lowercased. * * @return string Currency code lowercased. */ public function get_id(): string { return strtolower( $this->code ); } /** * Retrieves if the currency is default for the store. * * @return bool */ public function get_is_default(): bool { return $this->is_default; } /** * Retrieves the currency's name from WooCommerce core. * * @return string Currency name. */ public function get_name(): string { $wc_currencies = get_woocommerce_currencies(); return $wc_currencies[ $this->code ]; } /** * Retrieves the currency's conversion rate. * * @return float The conversion rate. */ public function get_rate(): float { return $this->rate; } /** * Retrieves the currency's rounding rate. * * @return string Rounding rate. */ public function get_rounding(): string { return (string) ( $this->rounding ?? '0' ); } /** * Retrieves the currency's symbol from WooCommerce core. * * @return string Currency symbol. */ public function get_symbol(): string { return get_woocommerce_currency_symbol( $this->code ); } /** * Retrieves the currency's symbol position from Localization Service * * @return string Currency position (left/right). */ public function get_symbol_position(): string { return $this->localization_service->get_currency_format( $this->code )['currency_pos']; } /** * Retrieves if the currency is zero decimal. * * @return bool */ public function get_is_zero_decimal(): bool { return $this->is_zero_decimal; } /** * Get the timestamp reprenting when the currency was last updated. * * @return int|null A timestamp representing when the currency was last updated. */ public function get_last_updated() { return $this->last_updated; } /** * Sets the currency's charm rate. * * @param float $charm Charm rate. */ public function set_charm( $charm ) { $this->charm = $charm; } /** * Sets the currency's conversion rate. * * @param float $rate Conversion rate. */ public function set_rate( $rate ) { $this->rate = $rate; } /** * Sets the currency's rounding rate. * * @param string $rounding Rounding rate. * @return void */ public function set_rounding( $rounding ) { $this->rounding = $rounding; } /** * Set the currency's last updated timestamp. * * @param int $last_updated A timestamp representing when the currency was last updated. * * @return void */ public function set_last_updated( int $last_updated ) { $this->last_updated = $last_updated; } /** * Specify the data that should be serialized to JSON. * * @return array Serialized Currency object. */ public function jsonSerialize(): array { return [ 'code' => $this->code, 'rate' => $this->get_rate(), 'name' => html_entity_decode( $this->get_name() ), 'id' => $this->get_id(), 'is_default' => $this->get_is_default(), 'flag' => $this->get_flag(), 'symbol' => html_entity_decode( $this->get_symbol() ), 'symbol_position' => $this->get_symbol_position(), 'is_zero_decimal' => $this->get_is_zero_decimal(), 'last_updated' => $this->get_last_updated(), ]; } } includes/multi-currency/MultiCurrency.php 0000777 00000161732 15251156605 0014701 0 ustar 00 <?php /** * Class MultiCurrency * * @package WooCommerce\Payments\MultiCurrency */ namespace WCPay\MultiCurrency; use WCPay\MultiCurrency\Exceptions\InvalidCurrencyException; use WCPay\MultiCurrency\Exceptions\InvalidCurrencyRateException; use WCPay\MultiCurrency\Interfaces\MultiCurrencyAccountInterface; use WCPay\MultiCurrency\Interfaces\MultiCurrencyApiClientInterface; use WCPay\MultiCurrency\Interfaces\MultiCurrencyCacheInterface; use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface; use WCPay\MultiCurrency\Interfaces\MultiCurrencySettingsInterface; use WCPay\MultiCurrency\Logger; use WCPay\MultiCurrency\Notes\NoteMultiCurrencyAvailable; use WCPay\MultiCurrency\Utils; use WC_Payments_Features; defined( 'ABSPATH' ) || exit; /** * Class that controls Multi-Currency functionality. */ class MultiCurrency { const CURRENCY_SESSION_KEY = 'wcpay_currency'; const CURRENCY_META_KEY = 'wcpay_currency'; const FILTER_PREFIX = 'wcpay_multi_currency_'; const CUSTOMER_CURRENCIES_KEY = 'wcpay_multi_currency_stored_customer_currencies'; /** * The plugin's ID. * * @var string */ public $id = 'wcpay_multi_currency'; /** * Static flag to show if the currencies initialization has been completed * * @var bool */ protected static $is_initialized = false; /** * Compatibility instance. * * @var Compatibility */ protected $compatibility; /** * Geolocation instance. * * @var Geolocation */ protected $geolocation; /** * The Currency Switcher Widget instance. * * @var null|CurrencySwitcherWidget */ protected $currency_switcher_widget; /** * Gutenberg Block implementation of the Currency Switcher Widget instance. * * @var CurrencySwitcherBlock */ protected $currency_switcher_block; /** * Utils instance. * * @var Utils */ protected $utils; /** * FrontendPrices instance. * * @var FrontendPrices */ protected $frontend_prices; /** * FrontendCurrencies instance. * * @var FrontendCurrencies */ protected $frontend_currencies; /** * StorefrontIntegration instance. * * @var StorefrontIntegration */ protected $storefront_integration; /** * The available currencies. * * @var Currency[]|null */ protected $available_currencies; /** * The default currency. * * @var Currency|null */ protected $default_currency; /** * The enabled currencies. * * @var Currency[]|null */ protected $enabled_currencies; /** * Instance of MultiCurrencySettingsInterface. * * @var MultiCurrencySettingsInterface */ private $settings_service; /** * Client for making requests to the API * * @var MultiCurrencyApiClientInterface */ private $payments_api_client; /** * Instance of MultiCurrencyAccountInterface. * * @var MultiCurrencyAccountInterface */ private $payments_account; /** * Instance of MultiCurrencyLocalizationInterface. * * @var MultiCurrencyLocalizationInterface */ private $localization_service; /** * Instance of MultiCurrencyCacheInterface. * * @var MultiCurrencyCacheInterface */ private $cache; /** * Tracking instance. * * @var Tracking */ protected $tracking; /** * Simulation variables array. * * @var array */ protected $simulation_params = []; /** * Class constructor. * * @param MultiCurrencySettingsInterface $settings_service Settings service. * @param MultiCurrencyApiClientInterface $payments_api_client Payments API client. * @param MultiCurrencyAccountInterface $payments_account Payments Account instance. * @param MultiCurrencyLocalizationInterface $localization_service Localization Service instance. * @param MultiCurrencyCacheInterface $cache Cache instance. * @param Utils|null $utils Optional Utils instance. */ public function __construct( MultiCurrencySettingsInterface $settings_service, MultiCurrencyApiClientInterface $payments_api_client, MultiCurrencyAccountInterface $payments_account, MultiCurrencyLocalizationInterface $localization_service, MultiCurrencyCacheInterface $cache, ?Utils $utils = null ) { $this->settings_service = $settings_service; $this->payments_api_client = $payments_api_client; $this->payments_account = $payments_account; $this->localization_service = $localization_service; $this->cache = $cache; // If a Utils instance is not passed as argument, initialize it. This allows to mock it in tests. $this->utils = $utils ?? new Utils(); $this->geolocation = new Geolocation( $this->localization_service ); $this->compatibility = new Compatibility( $this, $this->utils ); $this->currency_switcher_block = new CurrencySwitcherBlock( $this, $this->compatibility ); } /** * Backwards compatibility for the old `instance()` static method. * * We need to use this as some plugins still call `MultiCurrency::instance()` directly. * * @return null|MultiCurrency - Main instance. */ public static function instance() { if ( function_exists( 'WC_Payments_Multi_Currency' ) ) { return WC_Payments_Multi_Currency(); } } /** * Initializes this class' WP hooks. * * @return void */ public function init_hooks() { if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) { add_filter( 'woocommerce_get_settings_pages', [ $this, 'init_settings_pages' ] ); // Enqueue the scripts after the main WC_Payments_Admin does. add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ], 20 ); } add_action( 'init', [ $this, 'init' ] ); add_action( 'rest_api_init', [ $this, 'init_rest_api' ] ); add_action( 'widgets_init', [ $this, 'init_widgets' ] ); $is_frontend_request = ! is_admin() && ! defined( 'DOING_CRON' ) && ! Utils::is_admin_api_request(); if ( $is_frontend_request || Utils::is_store_api_request() ) { // Make sure that this runs after the main init function. add_action( 'init', [ $this, 'update_selected_currency_by_url' ], 11 ); add_action( 'init', [ $this, 'update_selected_currency_by_geolocation' ], 12 ); add_action( 'init', [ $this, 'possible_simulation_activation' ], 13 ); add_action( 'woocommerce_created_customer', [ $this, 'set_new_customer_currency_meta' ] ); } if ( ! Utils::is_store_batch_request() && ! Utils::is_store_api_request() && WC()->is_rest_api_request() ) { if ( isset( $_GET['currency'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $get_currency_from_query_param = function () { $currency = sanitize_text_field( wp_unslash( $_GET['currency'] ) ); // phpcs:ignore WordPress.Security.NonceVerification return strtoupper( $currency ); }; add_filter( self::FILTER_PREFIX . 'override_selected_currency', $get_currency_from_query_param ); } else { // If the request is a REST API request, ensure we default to the store currency and leave price as-is. add_filter( self::FILTER_PREFIX . 'should_return_store_currency', '__return_true' ); add_filter( self::FILTER_PREFIX . 'should_convert_product_price', '__return_false' ); $get_default_currency_code = function () { return $this->get_default_currency()->get_code(); }; add_filter( self::FILTER_PREFIX . 'override_selected_currency', $get_default_currency_code ); } } add_filter( 'wcpay_payment_fields_js_config', [ $this, 'add_props_to_wcpay_js_config' ] ); $this->currency_switcher_block->init_hooks(); } /** * Called after the WooCommerce session has been initialized. Initialises the available currencies, * default currency and enabled currencies for the Multi-Currency plugin. * * @return void */ public function init() { $store_currency_updated = $this->check_store_currency_for_change(); $this->initialize_available_currencies(); $this->set_default_currency(); $this->initialize_enabled_currencies(); // If the store currency has been updated, we need to update the notice that will display any manual currencies. if ( $store_currency_updated ) { $this->update_manual_rate_currencies_notice_option(); } $admin_notices = new AdminNotices(); $user_settings = new UserSettings( $this ); new Analytics( $this, $this->settings_service ); $this->frontend_prices = new FrontendPrices( $this, $this->compatibility ); $this->frontend_currencies = new FrontendCurrencies( $this, $this->localization_service, $this->utils, $this->compatibility ); $this->tracking = new Tracking( $this ); // Init all the hooks. $admin_notices->init_hooks(); $user_settings->init_hooks(); $this->frontend_prices->init_hooks(); $this->frontend_currencies->init_hooks(); $this->tracking->init_hooks(); add_action( 'woocommerce_order_refunded', [ $this, 'add_order_meta_on_refund' ], 50, 2 ); // Check to make sure there are enabled currencies, then for Storefront being active, and then load the integration. $theme = wp_get_theme(); if ( 'storefront' === $theme->get_stylesheet() || 'storefront' === $theme->get_template() ) { $this->storefront_integration = new StorefrontIntegration( $this ); } if ( is_admin() ) { add_action( 'admin_init', [ $this, 'add_woo_admin_notes' ] ); } // Update the customer currencies option after an order status change. add_action( 'woocommerce_order_status_changed', [ $this, 'maybe_update_customer_currencies_option' ] ); static::$is_initialized = true; } /** * Initialize the REST API controller. * * @return void */ public function init_rest_api() { // Ensures we are not initializing our REST during `rest_preload_api_request`. // When constructors signature changes, in manual update scenarios we were run into fatals. // Those fatals are not critical, but it causes hickups in release process as catches unnecessary attention. if ( function_exists( 'get_current_screen' ) && get_current_screen() ) { return; } $api_controller = new RestController( $this ); $api_controller->register_routes(); } /** * Initialize the legacy widgets. * * @return void */ public function init_widgets() { // Register the legacy widget. $this->currency_switcher_widget = new CurrencySwitcherWidget( $this, $this->compatibility ); register_widget( $this->currency_switcher_widget ); } /** * Initialize the Settings Pages. * * @param array $settings_pages The settings pages. * * @return array The new settings pages. */ public function init_settings_pages( $settings_pages ): array { // We don't need to check if the payment provider is connected for the // Settings page generation on the incoming CLI and async job calls. if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'WPCOM_JOBS' ) && WPCOM_JOBS ) ) { return $settings_pages; } // Due to autoloader limitations, we shouldn't initiate MCCY settings if the plugin was just upgraded: // https://github.com/Automattic/woocommerce-payments/issues/9676. if ( did_action( 'upgrader_process_complete' ) ) { return $settings_pages; } if ( $this->payments_account->is_provider_connected() ) { $settings = new Settings( $this ); $settings->init_hooks(); $settings_pages[] = $settings; } else { $settings_onboard_cta = new SettingsOnboardCta( $this, $this->payments_account ); $settings_onboard_cta->init_hooks(); $settings_pages[] = $settings_onboard_cta; } return $settings_pages; } /** * Load the admin assets. * * @return void */ public function enqueue_admin_scripts() { global $current_tab; // Enqueue the settings JS and CSS only on the WCPay multi-currency settings page. if ( 'wcpay_multi_currency' !== $current_tab ) { return; } $this->register_admin_scripts(); wp_enqueue_script( 'WCPAY_MULTI_CURRENCY_SETTINGS' ); wp_enqueue_style( 'WCPAY_MULTI_CURRENCY_SETTINGS' ); } /** * Add multi-currency specific props to the WCPay JS config. * * @param array $config The JS config that will be loaded on the frontend. * * @return array The updated JS config. */ public function add_props_to_wcpay_js_config( $config ) { $config['isMultiCurrencyEnabled'] = true; return $config; } /** * Gets and caches the data for the currency rates from the server. * Will be returned as an array with two keys: * - 'currencies' (the currencies) * - 'updated' (when this data was fetched from the API). * * @return ?array */ public function get_cached_currencies() { // If connection to server cannot be established, payment provider is not connected, or the account is rejected, // return any data we have cached (expired or not) or null. if ( ! $this->payments_api_client->is_server_connected() || ! $this->payments_account->is_provider_connected() || $this->payments_account->is_account_rejected() ) { $cached_data = $this->cache->get( MultiCurrencyCacheInterface::CURRENCIES_KEY, true ); return $cached_data ?? null; } return $this->cache->get_or_add( MultiCurrencyCacheInterface::CURRENCIES_KEY, function () { try { $currency_data = $this->payments_api_client->get_currency_rates( strtolower( get_woocommerce_currency() ) ); return [ 'currencies' => $currency_data, 'updated' => time(), ]; } catch ( \Exception $e ) { return null; } }, function ( $data ) { return is_array( $data ) && isset( $data['currencies'], $data['updated'] ); } ); } /** * Returns the Compatibility instance. * * @return Compatibility */ public function get_compatibility() { return $this->compatibility; } /** * Returns the Currency Switcher Widget instance. * * @return CurrencySwitcherWidget|null */ public function get_currency_switcher_widget() { return $this->currency_switcher_widget; } /** * Returns the FrontendPrices instance. * * @return FrontendPrices */ public function get_frontend_prices(): FrontendPrices { return $this->frontend_prices; } /** * Returns the FrontendCurrencies instance. * * @return FrontendCurrencies */ public function get_frontend_currencies(): FrontendCurrencies { return $this->frontend_currencies; } /** * Returns the StorefrontIntegration instance. * * @return StorefrontIntegration|null */ public function get_storefront_integration() { return $this->storefront_integration; } /** * Generates the switcher widget markup. * * @param array $instance The widget's instance settings. * @param array $args The widget's arguments. * * @return string The widget markup. */ public function get_switcher_widget_markup( array $instance = [], array $args = [] ): string { /** * The spl_object_hash function is used here due to we register the widget with an instance of the widget and * not the class name of the widget. WordPress core takes the instance and passes it through spl_object_hash * to get a hash and adds that as the widget's name in the $wp_widget_factory->widgets[] array. In order to * call the_widget, you need to have the name of the widget, so we get the instance and hash to use. */ ob_start(); $currency_switcher_widget = $this->get_currency_switcher_widget(); if ( ! is_object( $currency_switcher_widget ) ) { Logger::notice( sprintf( 'Invalid widget markup. Widget instance must be type object, %s given.', gettype( $currency_switcher_widget ) ) ); return ob_get_clean(); } the_widget( spl_object_hash( $currency_switcher_widget ), apply_filters( self::FILTER_PREFIX . 'theme_widget_instance', $instance ), apply_filters( self::FILTER_PREFIX . 'theme_widget_args', $args ) ); return ob_get_clean(); } /** * Returns the store's current available, enabled, and default currencies. * * @return array */ public function get_store_currencies(): array { return [ 'available' => $this->get_available_currencies(), 'enabled' => $this->get_enabled_currencies(), 'default' => $this->get_default_currency(), ]; } /** * Gets the currency settings for a single currency. * * @param string $currency_code The currency code to get settings for. * * @return array The currency's settings. * * @throws InvalidCurrencyException */ public function get_single_currency_settings( string $currency_code ): array { // Confirm the currency code is valid before trying to get the settings. if ( ! array_key_exists( strtoupper( $currency_code ), $this->get_available_currencies() ) ) { $this->log_and_throw_invalid_currency_exception( __FUNCTION__, $currency_code ); } $currency_code = strtolower( $currency_code ); return [ 'exchange_rate_type' => get_option( 'wcpay_multi_currency_exchange_rate_' . $currency_code, 'automatic' ), 'manual_rate' => get_option( 'wcpay_multi_currency_manual_rate_' . $currency_code, null ), 'price_rounding' => get_option( 'wcpay_multi_currency_price_rounding_' . $currency_code, null ), 'price_charm' => get_option( 'wcpay_multi_currency_price_charm_' . $currency_code, null ), ]; } /** * Updates the currency settings for a single currency. * * @param string $currency_code The single currency code to be updated. * @param string $exchange_rate_type The exchange rate type setting. * @param float $price_rounding The price rounding setting. * @param float $price_charm The price charm setting. * @param ?float $manual_rate The manual rate setting, or null. * * @return void * * @throws InvalidCurrencyException * @throws InvalidCurrencyRateException */ public function update_single_currency_settings( string $currency_code, string $exchange_rate_type, float $price_rounding, float $price_charm, $manual_rate = null ) { // Confirm the currency code is valid before trying to update the settings. if ( ! array_key_exists( strtoupper( $currency_code ), $this->get_available_currencies() ) ) { $this->log_and_throw_invalid_currency_exception( __FUNCTION__, $currency_code ); } $currency_code = strtolower( $currency_code ); if ( 'manual' === $exchange_rate_type && ! is_null( $manual_rate ) ) { if ( ! is_numeric( $manual_rate ) || 0 >= $manual_rate ) { $message = 'Invalid manual currency rate passed to update_single_currency_settings: ' . $manual_rate; Logger::error( $message ); throw new InvalidCurrencyRateException( esc_html( $message ), 500 ); } update_option( 'wcpay_multi_currency_manual_rate_' . $currency_code, $manual_rate ); } update_option( 'wcpay_multi_currency_price_rounding_' . $currency_code, $price_rounding ); update_option( 'wcpay_multi_currency_price_charm_' . $currency_code, $price_charm ); if ( in_array( $exchange_rate_type, [ 'automatic', 'manual' ], true ) ) { update_option( 'wcpay_multi_currency_exchange_rate_' . $currency_code, esc_attr( $exchange_rate_type ) ); } } /** * Updates the customer currencies option. * * @param int $order_id The order ID. * * @return void */ public function maybe_update_customer_currencies_option( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } $currency = strtoupper( $order->get_currency() ); $currencies = self::get_all_customer_currencies(); // Skip if the currency is already in the list. if ( in_array( $currency, $currencies, true ) ) { return; } $currencies[] = $currency; update_option( self::CUSTOMER_CURRENCIES_KEY, $currencies ); } /** * Gets the currencies available. Initializes it if needed. * * @return Currency[] Array of Currency objects. */ public function get_available_currencies(): array { if ( null === $this->available_currencies ) { $this->init(); } return $this->available_currencies ?? []; } /** * Gets the store base currency. Initializes it if needed. * * @return Currency The store base currency. */ public function get_default_currency(): Currency { if ( null === $this->default_currency ) { $this->init(); } return $this->default_currency ?? new Currency( $this->localization_service, get_woocommerce_currency() ); } /** * Gets the currently enabled currencies. Initializes it if needed. * * @return Currency[] Array of Currency objects. */ public function get_enabled_currencies(): array { if ( null === $this->enabled_currencies ) { $this->init(); } return $this->enabled_currencies ?? []; } /** * Sets the enabled currencies for the store. * * @param string[] $currencies Array of currency codes to be enabled. * * @return void * * @throws InvalidCurrencyException */ public function set_enabled_currencies( $currencies = [] ) { // If curriencies is not an array, or if there are no currencies, just exit. if ( ! is_array( $currencies ) || 0 === count( $currencies ) ) { return; } // Confirm the currencies submitted are available/valid currencies. $invalid_currencies = array_diff( $currencies, array_keys( $this->get_available_currencies() ) ); if ( 0 < count( $invalid_currencies ) ) { $this->log_and_throw_invalid_currency_exception( __FUNCTION__, implode( ', ', $invalid_currencies ) ); } // Get the currencies that were removed before they are updated. $removed_currencies = array_diff( array_keys( $this->get_enabled_currencies() ), $currencies ); // Update the enabled currencies and reinitialize. update_option( $this->id . '_enabled_currencies', $currencies ); $this->initialize_enabled_currencies(); Logger::debug( 'Enabled currencies updated: ' . var_export( $currencies, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export ); // Now remove the removed currencies settings. if ( 0 < count( $removed_currencies ) ) { $this->remove_currencies_settings( $removed_currencies ); } } /** * Gets the user selected currency, or `$default_currency` if is not set. * * @return Currency */ public function get_selected_currency(): Currency { $multi_currency_code = $this->compatibility->override_selected_currency(); $currency_code = $multi_currency_code ? $multi_currency_code : $this->get_stored_currency_code(); return $this->get_enabled_currencies()[ $currency_code ] ?? $this->get_default_currency(); } /** * Update the selected currency from a currency code. * * @param string $currency_code Three letter currency code. * @param bool $persist_change Set true to store the change in the session cookie if it doesn't exist yet. * * @return void */ public function update_selected_currency( string $currency_code, bool $persist_change = true ) { $code = strtoupper( $currency_code ); $user_id = get_current_user_id(); $currency = $this->get_enabled_currencies()[ $code ] ?? null; if ( null === $currency ) { return; } // We discard the cache for the front-end. $this->frontend_currencies->selected_currency_changed(); // initializing the session (useful for Store API), // so that the selected currency (set as query string parameter) can be correctly set. if ( ! isset( WC()->session ) ) { WC()->initialize_session(); } if ( $this->get_stored_currency_code() !== $code && $persist_change ) { $this->frontend_currencies->clear_url_price_params(); } if ( 0 === $user_id && WC()->session ) { WC()->session->set( self::CURRENCY_SESSION_KEY, $currency->get_code() ); // Set the session cookie if is not yet to persist the selected currency. if ( ! WC()->session->has_session() && ! headers_sent() && $persist_change ) { $this->utils->set_customer_session_cookie( true ); } } elseif ( $user_id ) { update_user_meta( $user_id, self::CURRENCY_META_KEY, $currency->get_code() ); } // Recalculate cart when currency changes. if ( did_action( 'wp_loaded' ) ) { $this->recalculate_cart(); } else { add_action( 'wp_loaded', [ $this, 'recalculate_cart' ] ); } } /** * Update the selected currency from url param `currency`. * * @return void */ public function update_selected_currency_by_url() { if ( ! isset( $_GET['currency'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return; } $this->update_selected_currency( sanitize_text_field( wp_unslash( $_GET['currency'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification } /** * Update the selected currency from the user's geolocation country. * * @return void */ public function update_selected_currency_by_geolocation() { // We only want to automatically set the currency if the option is enabled and it shouldn't be disabled for any reason. if ( ! $this->is_using_auto_currency_switching() || $this->compatibility->should_disable_currency_switching() ) { return; } // Display notice, prevent duplicates in simulation. if ( ! has_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] ) ) { add_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] ); } // Update currency only if it's already not set. if ( $this->get_stored_currency_code() ) { return; } $currency = $this->geolocation->get_currency_by_customer_location(); if ( empty( $this->get_enabled_currencies()[ $currency ] ) ) { return; } $this->update_selected_currency( $currency, false ); } /** * Gets the configured value for apply charm pricing only to products. * * @return mixed The configured value. */ public function get_apply_charm_only_to_products() { return apply_filters( self::FILTER_PREFIX . 'apply_charm_only_to_products', true ); } /** * Gets the converted price using the current currency with the rounding and charm pricing settings. * * @param mixed $price The price to be converted. * @param string $type The type of price being converted. One of 'product', 'shipping', 'tax', 'coupon', or 'exchange_rate'. * * @return float The converted price. */ public function get_price( $price, string $type ): float { $supported_types = [ 'product', 'shipping', 'tax', 'coupon', 'exchange_rate' ]; $currency = $this->get_selected_currency(); if ( ! in_array( $type, $supported_types, true ) || $currency->get_is_default() ) { return (float) $price; } $converted_price = ( (float) $price ) * $currency->get_rate(); if ( 'tax' === $type || 'coupon' === $type || 'exchange_rate' === $type ) { // We must make sure the price is rounded properly before returning it, otherwise we // may end up with inconsistent prices in the cart. $num_decimals = absint( $this->localization_service->get_currency_format( $currency->get_code() )['num_decimals'] ); return round( $converted_price, $num_decimals ); } $charm_compatible_types = [ 'product', 'shipping' ]; $apply_charm_pricing = $this->get_apply_charm_only_to_products() ? 'product' === $type : in_array( $type, $charm_compatible_types, true ); return $this->get_adjusted_price( $converted_price, $apply_charm_pricing, $currency ); } /** * Gets a raw converted amount based on the amount and currency codes passed. * This is a helper method for external conversions, if needed. * * @param float $amount The amount to be converted. * @param string $to_currency The 3 letter currency code to convert the amount to. * @param string $from_currency The 3 letter currency code to convert the amount from. * * @return float The converted amount. * * @throws InvalidCurrencyException * @throws InvalidCurrencyRateException */ public function get_raw_conversion( float $amount, string $to_currency, string $from_currency = '' ): float { $enabled_currencies = $this->get_enabled_currencies(); // If the from_currency is not set, use the store currency. if ( '' === $from_currency ) { $from_currency = $this->get_default_currency()->get_code(); } // We throw an exception if either of the currencies are not enabled. $to_currency = strtoupper( $to_currency ); $from_currency = strtoupper( $from_currency ); foreach ( [ $to_currency, $from_currency ] as $code ) { if ( ! isset( $enabled_currencies[ $code ] ) ) { $this->log_and_throw_invalid_currency_exception( __FUNCTION__, $code ); } } // Get the rates. $to_currency_rate = $enabled_currencies[ $to_currency ]->get_rate(); $from_currency_rate = $enabled_currencies[ $from_currency ]->get_rate(); // Throw an exception in case from_currency_rate is less than or equal to 0. if ( 0 >= $from_currency_rate ) { $message = 'Invalid rate for from_currency in get_raw_conversion: ' . $from_currency_rate; Logger::error( $message ); throw new InvalidCurrencyRateException( esc_html( $message ), 500 ); } $amount = $amount * ( $to_currency_rate / $from_currency_rate ); return (float) $amount; } /** * Recalculates WooCommerce cart totals. * * @return void */ public function recalculate_cart() { if ( WC()->cart ) { WC()->cart->calculate_totals(); } } /** * When an order is refunded, a new psuedo order is created to represent the refund. * We want to check if the original order was a multi-currency order, and if so, copy the meta data * to the new order. * * @param int $order_id The order ID. * @param int $refund_id The refund order ID. */ public function add_order_meta_on_refund( $order_id, $refund_id ) { $default_currency = $this->get_default_currency(); $order = wc_get_order( $order_id ); $refund = wc_get_order( $refund_id ); // Do not add exchange rate if order was made in the store's default currency. if ( ! $order || ! $refund || $default_currency->get_code() === $order->get_currency() ) { return; } $order_exchange_rate = $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true ); $stripe_exchange_rate = $order->get_meta( '_wcpay_multi_currency_stripe_exchange_rate', true ); $order_default_currency = $order->get_meta( '_wcpay_multi_currency_order_default_currency', true ); $refund->update_meta_data( '_wcpay_multi_currency_order_exchange_rate', $order_exchange_rate ); $refund->update_meta_data( '_wcpay_multi_currency_order_default_currency', $order_default_currency ); if ( $stripe_exchange_rate ) { $refund->update_meta_data( '_wcpay_multi_currency_stripe_exchange_rate', $stripe_exchange_rate ); } $refund->save_meta_data(); } /** * Displays a notice on the frontend informing the customer of the * automatic currency switch. */ public function display_geolocation_currency_update_notice() { $current_currency = $this->get_selected_currency(); $store_currency = get_option( 'woocommerce_currency' ); $country = $this->geolocation->get_country_by_customer_location(); $geolocated_currency = $this->geolocation->get_currency_by_customer_location(); $currencies = get_woocommerce_currencies(); // Don't run next checks if simulation is enabled. if ( ! $this->is_simulation_enabled() ) { // Do not display notice if using the store's default currency. if ( $store_currency === $current_currency->get_code() ) { return; } // Do not display notice for other currencies than geolocated. if ( $current_currency->get_code() !== $geolocated_currency ) { return; } } $message = sprintf( /* translators: %1 User's country, %2 Selected currency name, %3 Default store currency name, %4 Link to switch currency */ __( 'We noticed you\'re visiting from %1$s. We\'ve updated our prices to %2$s for your shopping convenience. <a href="%4$s">Use %3$s instead.</a>', 'woocommerce-payments' ), apply_filters( self::FILTER_PREFIX . 'override_notice_country', WC()->countries->countries[ $country ] ), apply_filters( self::FILTER_PREFIX . 'override_notice_currency_name', $current_currency->get_name() ), esc_html( $currencies[ $store_currency ] ), esc_url( '?currency=' . $store_currency ) ); $notice_id = md5( $message ); echo '<p class="woocommerce-store-notice demo_store" data-notice-id="' . esc_attr( $notice_id . 2 ) . '" style="display:none;">'; // No need to escape here as the contents of $message is already escaped. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $message; echo ' <a href="#" class="woocommerce-store-notice__dismiss-link">' . esc_html__( 'Dismiss', 'woocommerce-payments' ) . '</a></p>'; } /** * Sets a new customer's currency meta to what's in their session. * This is needed for when a new user/customer is created during the checkout process. * * @param int $customer_id The user/customer id. * * @return void */ public function set_new_customer_currency_meta( $customer_id ) { $code = 0 !== $customer_id && WC()->session ? WC()->session->get( self::CURRENCY_SESSION_KEY ) : false; if ( $code ) { update_user_meta( $customer_id, self::CURRENCY_META_KEY, $code ); } } /** * Adds Multi-Currency notes to the WC-Admin inbox. * * @return void */ public function add_woo_admin_notes() { // Do not try to add notes on ajax requests to improve their performance. if ( wp_doing_ajax() ) { return; } if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) { NoteMultiCurrencyAvailable::set_account( $this->payments_account ); NoteMultiCurrencyAvailable::possibly_add_note(); } } /** * Removes Multi-Currency notes from the WC-Admin inbox. * * @return void */ public static function remove_woo_admin_notes() { if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) { NoteMultiCurrencyAvailable::possibly_delete_note(); } } /** * Checks if the merchant has enabled automatic currency switching and geolocation. * * @return bool */ public function is_using_auto_currency_switching(): bool { return 'yes' === get_option( $this->id . '_enable_auto_currency', 'no' ); } /** * Checks if the merchant has enabled the currency switcher widget. * * @return bool */ public function is_using_storefront_switcher(): bool { return 'yes' === get_option( $this->id . '_enable_storefront_switcher', 'no' ); } /** * Gets the store settings. * * @return array The store settings. */ public function get_settings() { return [ $this->id . '_enable_auto_currency' => $this->is_using_auto_currency_switching(), $this->id . '_enable_storefront_switcher' => $this->is_using_storefront_switcher(), 'site_theme' => wp_get_theme()->get( 'Name' ), 'date_format' => esc_attr( get_option( 'date_format', 'F j, Y' ) ), 'time_format' => esc_attr( get_option( 'time_format', 'g:i a' ) ), 'store_url' => esc_attr( get_page_uri( wc_get_page_id( 'shop' ) ) ), ]; } /** * Updates the store settings * * @param array $params Update requested values. * * @return void */ public function update_settings( $params ) { $updateable_options = [ 'wcpay_multi_currency_enable_auto_currency', 'wcpay_multi_currency_enable_storefront_switcher', ]; foreach ( $updateable_options as $key ) { if ( isset( $params[ $key ] ) ) { update_option( $key, sanitize_text_field( $params[ $key ] ) ); } } } /** * Load script with all required dependencies. * * @param string $handler Script handler. * @param string $script Script name relative to the plugin root. * @param array $additional_dependencies Additional dependencies. * * @return void */ public function register_script_with_dependencies( string $handler, string $script, array $additional_dependencies = [] ) { $script_file = $script . '.js'; $script_src_url = plugins_url( $script_file, $this->settings_service->get_plugin_file_path() ); $script_asset_path = plugin_dir_path( $this->settings_service->get_plugin_file_path() ) . $script . '.asset.php'; $script_asset = file_exists( $script_asset_path ) ? require $script_asset_path : [ 'dependencies' => [] ]; // nosemgrep: audit.php.lang.security.file.inclusion-arg -- server generated path is used. $all_dependencies = array_merge( $script_asset['dependencies'], $additional_dependencies ); wp_register_script( $handler, $script_src_url, $all_dependencies, $this->get_file_version( $script_file ), true ); } /** * Get the file modified time as a cache buster if we're in dev mode. * * @param string $file Local path to the file. * * @return string */ public function get_file_version( $file ) { $plugin_path = plugin_dir_path( $this->settings_service->get_plugin_file_path() ); if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $plugin_path . $file ) ) { return (string) filemtime( $plugin_path . trim( $file, '/' ) ); } return $this->settings_service->get_plugin_version(); } /** * Validates the given currency code. * * @param string $currency_code The currency code to check validity. * * @return string|false Returns back the currency code in uppercase letters if it's valid, or `false` if not. */ public function validate_currency_code( $currency_code ) { return array_key_exists( strtoupper( $currency_code ), $this->available_currencies ) ? strtoupper( $currency_code ) : false; } /** * Get simulation params from querystring and activate when needed * * @return void */ public function possible_simulation_activation() { // This is required in the MC onboarding simulation iframe. $this->simulation_params = $this->get_multi_currency_onboarding_simulation_variables(); if ( ! $this->is_simulation_enabled() ) { return; } // Modify the page links to deliver required params in the simulation. $this->add_simulation_params_to_preview_urls(); $this->simulate_client_currency(); } /** * Returns whether the simulation querystring param is set and active * * @return bool Whether the simulation is enabled or not */ public function is_simulation_enabled() { return 0 < count( $this->simulation_params ); } /** * Gets the Multi-Currency onboarding preview overrides from the querystring. * * @return array Override variables */ public function get_multi_currency_onboarding_simulation_variables() { $parameters = $_GET; // phpcs:ignore WordPress.Security.NonceVerification // Check if we are in a preview session, don't interfere with the main session. if ( ! isset( $parameters['is_mc_onboarding_simulation'] ) || ! (bool) $parameters['is_mc_onboarding_simulation'] ) { // Check if the page referer has the variables. $server = $_SERVER; // phpcs:ignore WordPress.Security.NonceVerification // Check if we are coming from a simulation session (if we don't have the necessary query strings). if ( isset( $server['HTTP_REFERER'] ) && 0 < strpos( $server['HTTP_REFERER'], 'is_mc_onboarding_simulation' ) ) { wp_parse_str( wp_parse_url( $server['HTTP_REFERER'], PHP_URL_QUERY ), $parameters ); if ( ! isset( $parameters['is_mc_onboarding_simulation'] ) || ! (bool) $parameters['is_mc_onboarding_simulation'] ) { return []; } } else { return []; } } // Define variables which can be overridden inside the preview session, with their sanitization methods. $possible_variables = [ 'enable_storefront_switcher' => 'wp_validate_boolean', 'enable_auto_currency' => 'wp_validate_boolean', ]; // Define the defaults if the parameter is missing in the request. $defaults = [ 'enable_storefront_switcher' => false, 'enable_auto_currency' => false, ]; // Prepare the params array. $values = []; // Walk through the querystring parameter possibilities, and prepare the params. foreach ( $possible_variables as $possible_variable => $sanitization_callback ) { // phpcs:disable WordPress.Security.NonceVerification if ( isset( $parameters[ $possible_variable ] ) ) { $values[ $possible_variable ] = $sanitization_callback( $parameters[ $possible_variable ] ); } else { // Append the default, the param is missing in the querystring. $values [ $possible_variable ] = $defaults[ $possible_variable ]; } } return $values; } /** * Checks if the currently displayed page is the WooCommerce Payments * settings page for the Multi-Currency settings. * * @return bool */ public function is_multi_currency_settings_page(): bool { global $current_screen, $current_tab; return ( is_admin() && $current_tab && $current_screen && 'wcpay_multi_currency' === $current_tab && 'woocommerce_page_wc-settings' === $current_screen->base ); } /** * Get all the currencies that have been used in the store. * * @return array */ public function get_all_customer_currencies(): array { global $wpdb; $currencies = get_option( self::CUSTOMER_CURRENCIES_KEY ); if ( self::is_customer_currencies_data_valid( $currencies ) ) { return array_map( 'strtoupper', $currencies ); } $currencies = $this->get_available_currencies(); $query_union = []; if ( class_exists( 'Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) { foreach ( $currencies as $currency ) { $query_union[] = $wpdb->prepare( "SELECT %s AS currency_code, EXISTS(SELECT currency FROM {$wpdb->prefix}wc_orders WHERE currency=%s LIMIT 1) AS exists_in_orders", $currency->code, $currency->code ); } } else { foreach ( $currencies as $currency ) { $query_union[] = $wpdb->prepare( "SELECT %s AS currency_code, EXISTS(SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key=%s AND meta_value=%s LIMIT 1) AS exists_in_orders", $currency->code, '_order_currency', $currency->code ); } } $sub_query = implode( ' UNION ALL ', $query_union ); $query = "SELECT currency_code FROM ( $sub_query ) as subquery WHERE subquery.exists_in_orders=1 ORDER BY currency_code ASC"; $currencies = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( self::is_customer_currencies_data_valid( $currencies ) ) { update_option( self::CUSTOMER_CURRENCIES_KEY, $currencies ); return array_map( 'strtoupper', $currencies ); } return []; } /** * Checks if there are additional currencies enabled beyond the store's default one. * * @return bool */ public function has_additional_currencies_enabled(): bool { $enabled_currencies = $this->get_enabled_currencies(); return count( $enabled_currencies ) > 1; } /** * Returns if the currency initializations are completed. * * @return bool If the initializations have been completed. */ public function is_initialized(): bool { return static::$is_initialized; } /** * Adjusts the given amount for the currently selected currency. * * Applies charm pricing if specified, and adjusts the amount according to * the selected currency's conversion rate. * * @param float $amount The original amount to adjust. * @param bool $apply_charm_pricing Optional. Whether to apply charm pricing to the adjusted amount. Default true. * @return float The amount adjusted for the selected currency. */ public function adjust_amount_for_selected_currency( $amount, $apply_charm_pricing = true ) { return $this->get_adjusted_price( $amount, $apply_charm_pricing, $this->get_selected_currency() ); } /** * Returns the amount with the backend format. * * @param float $amount The amount to format. * @param array $args The arguments to pass to wc_price. * * @return string The formatted amount. */ public function get_backend_formatted_wc_price( float $amount, array $args = [] ): string { return wc_price( $amount, $args ); } /** * Gets the price after adjusting it with the rounding and charm settings. * * @param float $price The price to be adjusted. * @param bool $apply_charm_pricing Whether charm pricing should be applied. * @param Currency $currency The currency to be used when adjusting. * * @return float The adjusted price. */ protected function get_adjusted_price( $price, $apply_charm_pricing, $currency ): float { $rounding = (float) $currency->get_rounding(); // If rounding is configured to be `0.00` we still need to round to the nearest lowest // currency denomination. // Otherwise we ceil the price to the configured rounding option. // NOTE: We don't round if currency rounding is > 0.00 because in those cases we want to // ceil the amount. For example: if $price = 1.251 and currency rounding = 0.25 we // want that amount ceiled to 1.50. If we round( 1.251 ) to 1.25 before ceiling the // price to the nearest 0.25 amount the final amount will be 1.25, which is incorrect. if ( 0.00 === $rounding ) { $num_decimals = absint( $this->localization_service->get_currency_format( $currency->get_code() )['num_decimals'] ); $price = round( $price, $num_decimals ); } else { $price = $this->ceil_price( $price, $rounding ); } if ( $apply_charm_pricing ) { $price += (float) $currency->get_charm(); } // Do not return negative prices (possible because of $currency->get_charm()). return max( 0, $price ); } /** * Ceils the price to the next number based on the rounding value. * * @param float $price The price to be ceiled. * @param float $rounding The rounding option. * * @return float The ceiled price. */ protected function ceil_price( float $price, float $rounding ): float { if ( 0.00 === $rounding ) { return $price; } return ceil( $price / $rounding ) * $rounding; } /** * Sets up the available currencies, which are alphabetical by name. * * @return void */ private function initialize_available_currencies() { // Add default store currency with a rate of 1.0. $woocommerce_currency = get_woocommerce_currency(); $this->available_currencies[ $woocommerce_currency ] = new Currency( $this->localization_service, $woocommerce_currency, 1.0 ); $available_currencies = []; $currencies = $this->get_account_available_currencies(); if ( ! empty( $currencies ) ) { $cache_data = $this->get_cached_currencies(); foreach ( $currencies as $currency_code ) { $currency_rate = $cache_data['currencies'][ $currency_code ] ?? 1.0; $update_time = $cache_data['updated'] ?? null; $new_currency = new Currency( $this->localization_service, $currency_code, $currency_rate, $update_time ); // Add this to our list of available currencies. $available_currencies[ $new_currency->get_name() ] = $new_currency; } } ksort( $available_currencies ); foreach ( $available_currencies as $currency ) { $this->available_currencies[ $currency->get_code() ] = $currency; } } /** * Sets up the enabled currencies. * * @return void */ private function initialize_enabled_currencies() { $available_currencies = $this->get_available_currencies(); $enabled_currency_codes = get_option( $this->id . '_enabled_currencies', [] ); $enabled_currency_codes = is_array( $enabled_currency_codes ) ? $enabled_currency_codes : []; $default_code = $this->get_default_currency()->get_code(); $default = []; $enabled_currency_codes[] = $default_code; // This allows to keep the alphabetical sorting by name. $enabled_currencies = array_filter( $available_currencies, function ( $currency ) use ( $enabled_currency_codes ) { return in_array( $currency->get_code(), $enabled_currency_codes, true ); } ); $this->enabled_currencies = []; foreach ( $enabled_currencies as $enabled_currency ) { // Get the charm and rounding for each enabled currency and add the currencies to the object property. $currency = clone $enabled_currency; $charm = get_option( $this->id . '_price_charm_' . $currency->get_id(), 0.00 ); $rounding = get_option( $this->id . '_price_rounding_' . $currency->get_id(), $currency->get_is_zero_decimal() ? '100' : '1.00' ); $currency->set_charm( $charm ); $currency->set_rounding( $rounding ); // If the currency is set to be manual, set the rate to the stored manual rate. $type = get_option( $this->id . '_exchange_rate_' . $currency->get_id(), 'automatic' ); if ( 'manual' === $type ) { $manual_rate = get_option( $this->id . '_manual_rate_' . $currency->get_id(), $currency->get_rate() ); $currency->set_rate( $manual_rate ); } $this->enabled_currencies[ $currency->get_code() ] = $currency; } // Set default currency to the top of the list. $default[ $default_code ] = $this->enabled_currencies[ $default_code ]; unset( $this->enabled_currencies[ $default_code ] ); $this->enabled_currencies = array_merge( $default, $this->enabled_currencies ); } /** * Sets the default currency. * * @return void */ private function set_default_currency() { $available_currencies = $this->get_available_currencies(); $this->default_currency = $available_currencies[ get_woocommerce_currency() ] ?? null; } /** * Returns the currency code stored for the user or in the session. * * @return string|null Currency code. */ private function get_stored_currency_code() { $user_id = get_current_user_id(); if ( $user_id ) { return get_user_meta( $user_id, self::CURRENCY_META_KEY, true ); } WC()->initialize_session(); $currency_code = WC()->session->get( self::CURRENCY_SESSION_KEY ); return is_string( $currency_code ) ? $currency_code : null; } /** * Checks to see if the store currency has changed. If it has, this will * also update the option containing the store currency. * * @return bool */ private function check_store_currency_for_change(): bool { $last_known_currency = get_option( $this->id . '_store_currency', false ); $woocommerce_currency = get_woocommerce_currency(); // If the last known currency was not set, update the option to set it and return false. if ( ! $last_known_currency ) { update_option( $this->id . '_store_currency', $woocommerce_currency ); return false; } if ( $last_known_currency !== $woocommerce_currency ) { update_option( $this->id . '_store_currency', $woocommerce_currency ); return true; } return false; } /** * Called when the store currency has changed. Puts any manual rate currencies into an option for a notice to display. * * @return void */ private function update_manual_rate_currencies_notice_option() { $enabled_currencies = $this->get_enabled_currencies(); $manual_currencies = []; // Check enabled currencies for manual rates. foreach ( $enabled_currencies as $currency ) { $rate_type = get_option( $this->id . '_exchange_rate_' . $currency->get_id(), false ); if ( 'manual' === $rate_type ) { $manual_currencies[] = $currency->get_name(); } } if ( 0 < count( $manual_currencies ) ) { update_option( $this->id . '_show_store_currency_changed_notice', $manual_currencies ); } } /** * Accepts an array of currencies that should have their settings removed. * * @param array $currencies Array of Currency objects or 3 letter currency codes. * * @return void */ private function remove_currencies_settings( array $currencies ) { foreach ( $currencies as $currency ) { $this->remove_currency_settings( $currency ); } } /** * Will remove a currency's settings if it is not enabled. * * @param mixed $currency Currency object or 3 letter currency code. * * @return void */ private function remove_currency_settings( $currency ) { $code = is_a( $currency, Currency::class ) ? $currency->get_code() : strtoupper( $currency ); // Bail if the currency code passed is not 3 characters, or if the currency is presently enabled. if ( 3 !== strlen( $code ) || isset( $this->get_enabled_currencies()[ $code ] ) ) { return; } $settings = [ 'price_charm', 'price_rounding', 'manual_rate', 'exchange_rate', ]; // Go through each setting and remove them. foreach ( $settings as $setting ) { delete_option( $this->id . '_' . $setting . '_' . strtolower( $code ) ); } } /** * Returns the currencies enabled for the payment provider account that are * also available in WC. * * Can be filtered with the 'wcpay_multi_currency_available_currencies' hook. * * @return array Array with the available currencies' codes. */ private function get_account_available_currencies(): array { // If the payment provider is not connected, return an empty array. This prevents using MC without being connected to the payment provider. if ( ! $this->payments_account->is_provider_connected() ) { return []; } $wc_currencies = array_keys( get_woocommerce_currencies() ); $account_currencies = $wc_currencies; $account = $this->payments_account->get_cached_account_data(); $supported_currencies = $this->payments_account->get_account_customer_supported_currencies(); if ( $account && ! empty( $supported_currencies ) ) { $account_currencies = array_map( 'strtoupper', $supported_currencies ); } /** * Filter the available currencies for WooCommerce Multi-Currency. * * This filter can be used to modify the currencies available for WC Pay * Multi-Currency. Currencies have to be added in uppercase and should * also be available in `get_woocommerce_currencies` for them to work. * * @since 2.8.0 * * @param array $available_currencies Current available currencies. Calculated based on * WC Pay's account currencies and WC currencies. */ return apply_filters( self::FILTER_PREFIX . 'available_currencies', array_intersect( $account_currencies, $wc_currencies ) ); } /** * Register the CSS and JS admin scripts. * * @return void */ private function register_admin_scripts() { $this->register_script_with_dependencies( 'WCPAY_MULTI_CURRENCY_SETTINGS', 'dist/multi-currency', [ 'WCPAY_ADMIN_SETTINGS', 'wp-components' ] ); wp_register_style( 'WCPAY_MULTI_CURRENCY_SETTINGS', plugins_url( 'dist/multi-currency.css', $this->settings_service->get_plugin_file_path() ), [ 'wc-components', 'WCPAY_ADMIN_SETTINGS' ], $this->get_file_version( 'dist/multi-currency.css' ), 'all' ); } /** * Enables simulation of client browser currency. * * @return void */ private function simulate_client_currency() { if ( ! $this->simulation_params['enable_auto_currency'] ) { return; } $countries = $this->payments_account->get_supported_countries(); $predefined_simulation_currencies = [ 'USD' => $countries['US'], 'GBP' => $countries['GB'], ]; $simulation_currency = 'USD' === get_option( 'woocommerce_currency', 'USD' ) ? 'GBP' : 'USD'; $simulation_currency_name = $this->available_currencies[ $simulation_currency ]->get_name(); $simulation_country = $predefined_simulation_currencies[ $simulation_currency ]; // Simulate client currency from geolocation. add_filter( 'wcpay_multi_currency_override_notice_currency_name', function ( $selected_currency_name ) use ( $simulation_currency_name ) { return $simulation_currency_name; } ); // Simulate client country from geolocation. add_filter( 'wcpay_multi_currency_override_notice_country', function ( $selected_country ) use ( $simulation_country ) { return $simulation_country; } ); // Always display the notice on simulation screen, prevent duplicate hooks. if ( ! has_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] ) ) { add_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] ); } // Skip recalculating the cart to prevent infinite loop in simulation. remove_action( 'wp_loaded', [ $this, 'recalculate_cart' ] ); } /** * Adds the required querystring parameters to all urls in preview pages. * * @return void */ private function add_simulation_params_to_preview_urls() { $params = $this->simulation_params; add_filter( 'wp_footer', function () use ( $params ) { ?> <script type="text/javascript" id="wcpay_multi_currency-simulation-script"> // Add simulation overrides to all links. document.querySelectorAll('a').forEach((link) => { const parsedURL = new URL(link.href); if ( false === parsedURL.searchParams.has( 'is_mc_onboarding_simulation' ) ) { parsedURL.searchParams.set('is_mc_onboarding_simulation', true); parsedURL.searchParams.set('enable_auto_currency', <?php echo esc_attr( $params['enable_auto_currency'] ? 'true' : 'false' ); ?>); parsedURL.searchParams.set('enable_storefront_switcher', <?php echo esc_attr( $params['enable_storefront_switcher'] ? 'true' : 'false' ); ?>); link.href = parsedURL.toString(); } }); // Unhide the store notice in simulation mode. document.addEventListener('DOMContentLoaded', () => { const noticeElement = document.querySelector('.woocommerce-store-notice.demo_store') if( noticeElement ) { const noticeId = noticeElement.getAttribute('data-notice-id'); cookieStore.delete( 'store_notice' + noticeId ); } }); </script> <?php } ); } /** * Logs a message and throws InvalidCurrencyException. * * @param string $method The method that's actually throwing the exception. * @param string $currency_code The currency code that was invalid. * @param int $code The exception code. * * @throws InvalidCurrencyException */ private function log_and_throw_invalid_currency_exception( $method, $currency_code, $code = 500 ) { $message = 'Invalid currency passed to ' . $method . ': ' . $currency_code; Logger::error( $message ); throw new InvalidCurrencyException( esc_html( $message ), esc_html( $code ) ); } /** * Checks if the customer currencies data is valid. * * @param mixed $currencies The currencies to check. * * @return bool */ private function is_customer_currencies_data_valid( $currencies ) { return ! empty( $currencies ) && is_array( $currencies ); } } includes/multi-currency/StorefrontIntegration.php 0000777 00000006626 15251156605 0016445 0 ustar 00 <?php /** * Class StorefrontIntegration * * @package WooCommerce\Payments\StorefrontIntegration */ namespace WCPay\MultiCurrency; defined( 'ABSPATH' ) || exit; /** * Class that controls Multi-Currency Storefront Integration. */ class StorefrontIntegration { /** * Multi-Currency instance. * * @var MultiCurrency */ protected $multi_currency; /** * Constructor. * * @param MultiCurrency $multi_currency The MultiCurrency instance. */ public function __construct( MultiCurrency $multi_currency ) { $this->multi_currency = $multi_currency; $this->init_actions_and_filters(); } /** * Adds the CSS to the head of the page. * * @return void */ public function add_inline_css() { $css = ' #woocommerce-payments-multi-currency-storefront-widget { float: right; } #woocommerce-payments-multi-currency-storefront-widget form { margin: 0; } '; wp_add_inline_style( 'storefront-style', apply_filters( MultiCurrency::FILTER_PREFIX . 'storefront_widget_css', $css ) ); } /** * This modifies the breadcrumb defaults for us to be able to place the widget. * * @param array $defaults The defaults breadcrumb properties. * * @return array The modified defaults properties. */ public function modify_breadcrumb_defaults( array $defaults ): array { // Set the instance and args arrays for the widget. $instance = apply_filters( MultiCurrency::FILTER_PREFIX . 'storefront_widget_instance', [] ); $args = apply_filters( MultiCurrency::FILTER_PREFIX . 'storefront_widget_args', [ 'before_widget' => '<div id="woocommerce-payments-multi-currency-storefront-widget" class="woocommerce-breadcrumb">', 'after_widget' => '</div>', ] ); /** * Some storefront child themes use different wrappers and styles. We need to place the widget before * the <nav> to display it properly. */ $defaults['wrap_before'] = str_replace( '<nav', $this->multi_currency->get_switcher_widget_markup( $instance, $args ) . '<nav', $defaults['wrap_before'] ); return $defaults; } /** * Adds the actions and filters. * * @return void */ private function init_actions_and_filters() { // Do not enable the breadcrumb widget if there's only one currency active. if ( 1 >= count( $this->multi_currency->get_enabled_currencies() ) ) { return; } // Simulation overrides for Multi-Currency onboarding preview. $simulation_variables = $this->multi_currency->get_multi_currency_onboarding_simulation_variables() ?? []; $simulation_enabled = false; $simulation_hide_switcher = false; if ( 0 < count( $simulation_variables ) && isset( $simulation_variables['enable_storefront_switcher'] ) ) { // We have a incoming override request! Simulate the flag. $simulation_enabled = true; $enable_storefront_switcher = (bool) $simulation_variables['enable_storefront_switcher']; // If the Storefront switcher is not enabled on the onboarding page, hide it. if ( ! $enable_storefront_switcher ) { $simulation_hide_switcher = true; } } // We want this enabled by default, so we default the option to 'yes'. if ( ! $simulation_hide_switcher && ( $simulation_enabled || $this->multi_currency->is_using_storefront_switcher() ) ) { add_filter( 'woocommerce_breadcrumb_defaults', [ $this, 'modify_breadcrumb_defaults' ], 9999 ); add_action( 'wp_enqueue_scripts', [ $this, 'add_inline_css' ], 50 ); } } } includes/multi-currency/AdminNotices.php 0000777 00000006476 15251156605 0014454 0 ustar 00 <?php /** * Admin notices for Multi-Currency. * * @package WooCommerce\Payments\MultiCurrency */ namespace WCPay\MultiCurrency; defined( 'ABSPATH' ) || exit; /** * Class that will display admin notices. */ class AdminNotices { /** * Notices. * * @var array */ private $notices = []; /** * Initializes this class' WP hooks. * * @return void */ public function init_hooks() { add_action( 'admin_notices', [ $this, 'admin_notices' ] ); add_action( 'wp_loaded', [ $this, 'hide_notices' ] ); } /** * Display any notices we've collected thus far. */ public function admin_notices() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } $this->check_for_notices(); foreach ( $this->notices as $notice_key => $notice ) { echo '<div class="' . esc_attr( $notice['class'] ) . '" style="position:relative;">'; if ( $notice['dismissible'] ) { ?> <a href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wcpay-multi-currency-hide-notice', $notice_key ), 'wcpay_multi_currency_hide_notices_nonce', '_wcpay_multi_currency_notice_nonce' ) ); ?>" class="woocommerce-message-close notice-dismiss" style="position:relative;float:right;padding:9px 0 9px 9px;text-decoration:none;"></a> <?php } echo '<p>'; echo wp_kses( $notice['message'], [ 'a' => [ 'href' => [], 'target' => [], ], ] ); echo '</p></div>'; } } /** * Hides any admin notices. */ public function hide_notices() { if ( isset( $_GET['wcpay-multi-currency-hide-notice'] ) && isset( $_GET['_wcpay_multi_currency_notice_nonce'] ) ) { if ( ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wcpay_multi_currency_notice_nonce'] ) ), 'wcpay_multi_currency_hide_notices_nonce' ) ) { wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce-payments' ) ); } if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Cheatin’ huh?', 'woocommerce-payments' ) ); } $notice = wc_clean( wp_unslash( $_GET['wcpay-multi-currency-hide-notice'] ) ); if ( 'currency_changed' === $notice ) { update_option( 'wcpay_multi_currency_show_store_currency_changed_notice', 'no' ); } } } /** * Adds admin notice to be displayed. * * @param string $slug Slug for the notice. * @param string $class Class(es) for the notice. * @param string $message Message in the notice. * @param bool $dismissible Whether the notice can be dismissed or not. */ private function add_admin_notice( $slug, $class, $message, $dismissible = false ) { $this->notices[ $slug ] = [ 'class' => $class, 'message' => $message, 'dismissible' => $dismissible, ]; } /** * Checks for notices and add them. */ private function check_for_notices() { $manual_currencies = get_option( 'wcpay_multi_currency_show_store_currency_changed_notice', false ); if ( is_array( $manual_currencies ) ) { $currencies = implode( ', ', $manual_currencies ); // translators: %s List of currencies that are already translated in WooCommerce core. $this->add_admin_notice( 'currency_changed', 'notice notice-warning', sprintf( __( 'The store currency was recently changed. The following currencies are set to manual rates and may need updates: %s', 'woocommerce-payments' ), $currencies ), true ); } } } includes/multi-currency/Geolocation.php 0000777 00000004511 15251156605 0014326 0 ustar 00 <?php /** * WooCommerce Payments Multi-Currency Geolocation Class * * @package WooCommerce\Payments */ namespace WCPay\MultiCurrency; use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface; defined( 'ABSPATH' ) || exit; /** * Geolocation. */ class Geolocation { /** * MultiCurrencyLocalizationInterface instance. * * @var MultiCurrencyLocalizationInterface */ protected $localization_service; /** * Constructor. * * @param MultiCurrencyLocalizationInterface $localization_service The Localization Service instance. */ public function __construct( MultiCurrencyLocalizationInterface $localization_service ) { $this->localization_service = $localization_service; } /** * Gets the customer's currency based on their location. * * @return string|null Currency code or null if not found. */ public function get_currency_by_customer_location() { $country = $this->get_country_by_customer_location(); return $this->localization_service->get_country_locale_data( $country )['currency_code'] ?? null; } /** * Gets the customer's country based on their location. * * @return string Country code. */ public function get_country_by_customer_location() { $country = $this->geolocate_customer(); if ( $country ) { // Once we have a location, ensure it's valid, otherwise fallback to the default country. $allowed_country_codes = WC()->countries->get_allowed_countries(); if ( ! array_key_exists( $country, $allowed_country_codes ) ) { $country = null; } } if ( ! $country ) { $default_location = get_option( 'woocommerce_default_country', '' ); $location = wc_format_country_state_string( apply_filters( 'woocommerce_customer_default_location', $default_location ) ); $country = $location['country']; } return $country; } /** * Attempts to guess the customer's country based on their IP. * * @return string|null Country code, or NULL if it couldn't be determined. */ private function geolocate_customer() { // Exclude common bots from geolocation by user agent. $ua = wc_get_user_agent(); if ( stripos( $ua, 'bot' ) !== false || stripos( $ua, 'spider' ) !== false || stripos( $ua, 'crawl' ) !== false ) { return null; } $geolocation = \WC_Geolocation::geolocate_ip( '', true, true ); return $geolocation['country'] ?? null; } } includes/multi-currency/README.md 0000777 00000000400 15251156605 0012622 0 ustar 00 # WooPayments multi-currency directory This directory contains the multi-currency module, which has been decoupled and extracted from the gateway code. The module is responsible for handling all multi-currency functionality, both back-end and front-end. includes/multi-currency/Notes/NoteMultiCurrencyAvailable.php 0000777 00000004267 15251156605 0020417 0 ustar 00 <?php /** * Notify merchant that Multi-Currency is available. * * @package WooCommerce\Payments\MultiCurrency */ namespace WCPay\MultiCurrency\Notes; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; use WCPay\MultiCurrency\Interfaces\MultiCurrencyAccountInterface; defined( 'ABSPATH' ) || exit; /** * Class NoteMultiCurrencyAvailable */ class NoteMultiCurrencyAvailable { use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-payments-notes-multi-currency-available'; /** * Url to start the setup process. Now redirects to the wizard page. */ // TODO: Proper url needed for setup process. const NOTE_SETUP_URL = 'admin.php?page=wc-admin&path=/payments/multi-currency-setup'; /** * The account service instance. * * @var MultiCurrencyAccountInterface */ private static $account; /** * Get the note. */ public static function get_note() { $note = new Note(); $note->set_title( __( 'Sell worldwide in multiple currencies', 'woocommerce-payments' ) ); $note->set_content( __( 'Boost your international sales by allowing your customers to shop and pay in their local currency.', 'woocommerce-payments' ) ); $note->set_content_data( (object) [] ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-payments' ); $note->add_action( self::NOTE_NAME, __( 'Set up now', 'woocommerce-payments' ), self::NOTE_SETUP_URL, 'unactioned', true ); return $note; } /** * Add the note if it passes predefined conditions. */ public static function possibly_add_note() { // Don't add the note if the merchant didn't create a WCPay account yet. if ( is_null( self::$account ) || ! self::$account->is_provider_connected() ) { return; } if ( ! self::can_be_added() ) { return; } $note = self::get_note(); $note->save(); } /** * Sets the account service instance reference on the class. * * @param MultiCurrencyAccountInterface $account account service instance. */ public static function set_account( MultiCurrencyAccountInterface $account ) { self::$account = $account; } } includes/multi-currency/RestController.php 0000777 00000016041 15251156605 0015045 0 ustar 00 <?php /** * Class RestController * * @package WooCommerce\Payments\MultiCurrency */ namespace WCPay\MultiCurrency; use Exception; use WCPay\MultiCurrency\Exceptions\InvalidCurrencyException; use WCPay\MultiCurrency\MultiCurrency; defined( 'ABSPATH' ) || exit; /** * REST controller for multi-currency. */ class RestController extends \WP_REST_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'wc/v3'; /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/multi-currency'; /** * MultiCurrency instance. * * @var MultiCurrency */ protected $multi_currency; /** * Constructor. * * @param MultiCurrency $multi_currency MultiCurrency instance. */ public function __construct( MultiCurrency $multi_currency ) { $this->multi_currency = $multi_currency; } /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/currencies', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_store_currencies' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/update-enabled-currencies', [ 'methods' => \WP_REST_Server::CREATABLE, 'args' => [ 'enabled' => [ 'type' => 'array', 'required' => true, ], ], 'callback' => [ $this, 'update_enabled_currencies' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/currencies/(?P<currency_code>[A-Za-z]{3})', [ 'methods' => \WP_REST_Server::READABLE, 'args' => [ 'currency_code' => [ 'type' => 'string', 'format' => 'text-field', 'required' => true, ], ], 'callback' => [ $this, 'get_single_currency_settings' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/get-settings', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_settings' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/currencies/(?P<currency_code>[A-Za-z]{3})', [ 'methods' => \WP_REST_Server::CREATABLE, 'args' => [ 'currency_code' => [ 'type' => 'string', 'format' => 'text-field', 'required' => true, ], 'exchange_rate_type' => [ 'type' => 'string', 'format' => 'text-field', 'required' => true, ], 'manual_rate' => [ 'type' => 'number', 'required' => false, ], 'price_rounding' => [ 'type' => 'number', 'required' => true, ], 'price_charm' => [ 'type' => 'number', 'required' => true, ], ], 'callback' => [ $this, 'update_single_currency_settings' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/update-settings', [ 'methods' => \WP_REST_Server::CREATABLE, 'args' => [ 'wcpay_multi_currency_enable_auto_currency' => [ 'type' => 'string', 'format' => 'text-field', 'required' => true, ], 'wcpay_multi_currency_enable_storefront_switcher' => [ 'type' => 'string', 'format' => 'text-field', 'required' => true, ], ], 'callback' => [ $this, 'update_settings' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Retrieve currencies for the store. * * @return \WP_REST_Response|\WP_Error Array of the store currencies structure. */ public function get_store_currencies() { return rest_ensure_response( $this->multi_currency->get_store_currencies() ); } /** * Update enabled currencies based on posted data. * * @param \WP_REST_Request $request Full data about the request. * * @return \WP_REST_Response|\WP_Error The store currencies structure or WP_Error. */ public function update_enabled_currencies( $request ) { $enabled = $request->get_param( 'enabled' ); try { $this->multi_currency->set_enabled_currencies( $enabled ); $response = $this->get_store_currencies(); } catch ( InvalidCurrencyException $e ) { $response = new \WP_Error( $e->getCode(), $e->getMessage() ); } return rest_ensure_response( $response ); } /** * Gets the currency settings for a single currency. * * @param \WP_REST_Request $request Full data about the request. * * @return \WP_REST_Response|\WP_Error The single currency settings as an array. */ public function get_single_currency_settings( $request ) { $currency_code = $request->get_param( 'currency_code' ); try { $response = $this->multi_currency->get_single_currency_settings( $currency_code ); } catch ( InvalidCurrencyException $e ) { $response = new \WP_Error( $e->getCode(), $e->getMessage() ); } return rest_ensure_response( $response ); } /** * Updates the currency settings for a single currency. * * @param \WP_REST_Request $request Full data about the request. * * @return \WP_REST_Response|\WP_Error The single currency settings as an array. */ public function update_single_currency_settings( $request ) { $currency_code = $request->get_param( 'currency_code' ); $exchange_rate_type = $request->get_param( 'exchange_rate_type' ); $price_rounding = $request->get_param( 'price_rounding' ); $price_charm = $request->get_param( 'price_charm' ); $manual_rate = $request->get_param( 'manual_rate' ) ?? null; try { $this->multi_currency->update_single_currency_settings( $currency_code, $exchange_rate_type, $price_rounding, $price_charm, $manual_rate ); $response = $this->multi_currency->get_single_currency_settings( $currency_code ); } catch ( Exception $e ) { $response = new \WP_Error( $e->getCode(), $e->getMessage() ); } return rest_ensure_response( $response ); } /** * Gets the store settings for Multi-Currency. * * @return \WP_REST_Response|\WP_Error The store settings as an array. */ public function get_settings() { return rest_ensure_response( $this->multi_currency->get_settings() ); } /** * Updates Multi-Currency store settings parameters. * * @param \WP_REST_Request $request Full data about the request. * * @return \WP_REST_Response|\WP_Error The store settings as an array. */ public function update_settings( $request ) { $params = $request->get_params(); $this->multi_currency->update_settings( $params ); return rest_ensure_response( $this->multi_currency->get_settings() ); } /** * Verify access. */ public function check_permission() { return current_user_can( 'manage_woocommerce' ); } } includes/multi-currency/UserSettings.php 0000777 00000005013 15251156605 0014520 0 ustar 00 <?php /** * WooCommerce Payments Multi-Currency User Settings * * @package WooCommerce\Payments */ namespace WCPay\MultiCurrency; defined( 'ABSPATH' ) || exit; /** * Class that add Multi-Currency settings to user my account page. */ class UserSettings { /** * Multi-Currency instance. * * @var MultiCurrency */ protected $multi_currency; /** * Constructor. * * @param MultiCurrency $multi_currency The MultiCurrency instance. */ public function __construct( MultiCurrency $multi_currency ) { $this->multi_currency = $multi_currency; } /** * Initializes this class' WP hooks. * * @return void */ public function init_hooks() { // Only show currency selector if more than one currency is enabled. if ( 1 < count( $this->multi_currency->get_enabled_currencies() ) ) { add_action( 'woocommerce_edit_account_form', [ $this, 'add_presentment_currency_switch' ] ); add_action( 'woocommerce_save_account_details', [ $this, 'save_presentment_currency' ] ); } } /** * Add a select to allow user choose default currency in `my account > account details`. * * @return void */ public function add_presentment_currency_switch() { ?> <p class="woocommerce-form-row woocommerce-form-row--first form-row form-row-first"> <label for="wcpay_selected_currency"><?php esc_html_e( 'Default currency', 'woocommerce-payments' ); ?></label> <select name="wcpay_selected_currency" id="wcpay_selected_currency" > <?php foreach ( $this->multi_currency->get_enabled_currencies() as $currency ) { $code = $currency->get_code(); $symbol = $currency->get_symbol(); $selected = $this->multi_currency->get_selected_currency()->code === $code ? ' selected' : ''; echo "<option value=\"$code\"$selected>$symbol $code</option>"; // phpcs:ignore WordPress.Security.EscapeOutput } ?> </select> <span><em><?php esc_html_e( 'Select your preferred currency for shopping and payments.', 'woocommerce-payments' ); ?></em></span> </p> <div class="clear"></div> <?php } /** * Hook into save account details to capture the new value `wcpay_selected_currency` and persist it. * * @return void */ public function save_presentment_currency() { if ( isset( $_POST['wcpay_selected_currency'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $currency_code = wc_clean( wp_unslash( $_POST['wcpay_selected_currency'] ) ); // phpcs:ignore WordPress.Security.NonceVerification $this->multi_currency->update_selected_currency( $currency_code ); } } } includes/multi-currency/Interfaces/MultiCurrencyApiClientInterface.php 0000777 00000001351 15251156605 0022364 0 ustar 00 <?php /** * Interface MultiCurrencyApiClientInterface * * @package WooCommerce\Payments\MultiCurrency\Interfaces */ namespace WCPay\MultiCurrency\Interfaces; defined( 'ABSPATH' ) || exit; interface MultiCurrencyApiClientInterface { /** * Whether the API client is connected to the server. * * @return bool */ public function is_server_connected(): bool; /** * Get currency rates from the server. * * @param string $currency_from - The currency to convert from. * @param ?array $currencies_to - An array of the currencies we want to convert into. If left empty, will get all supported currencies. * * @return array */ public function get_currency_rates( string $currency_from, $currencies_to = null ): array; } includes/multi-currency/Interfaces/MultiCurrencyAccountInterface.php 0000777 00000003406 15251156605 0022113 0 ustar 00 <?php /** * Interface MultiCurrencyAccountInterface * * @package WooCommerce\Payments\MultiCurrency\Interfaces */ namespace WCPay\MultiCurrency\Interfaces; defined( 'ABSPATH' ) || exit; interface MultiCurrencyAccountInterface { /** * Checks if the account is connected to the payment provider. * * @param bool $on_error Value to return on server error, defaults to false. * * @return bool True if the account is connected, false otherwise, $on_error on error. */ public function is_provider_connected( bool $on_error = false ): bool; /** * Checks if the account has been rejected, assumes the value of false on any account retrieval error. * Returns false if the account is not connected. * * Note: We might want to use a more generic method to check if the account is in an enabled or * disabled state for better compatibility between V1 and V2. * * @return bool True if the account is connected and rejected, false otherwise or on error. */ public function is_account_rejected(): bool; /** * Gets and caches the data for the account connected to this site. * * @param bool $force_refresh Forces data to be fetched from the server, rather than using the cache. * * @return array|bool Account data or false if failed to retrieve account data. */ public function get_cached_account_data( bool $force_refresh = false ); /** * Gets the customer currencies supported for the account. * * @return array Currencies. */ public function get_account_customer_supported_currencies(): array; /** * Get list of countries supported by the provider. */ public function get_supported_countries(): array; /** * Get provider onboarding page url. * * @return string */ public function get_provider_onboarding_page_url(): string; } includes/multi-currency/Interfaces/MultiCurrencySettingsInterface.php 0000777 00000001070 15251156605 0022312 0 ustar 00 <?php /** * Interface MultiCurrencySettingsInterface * * @package WooCommerce\Payments\MultiCurrency\Interfaces */ namespace WCPay\MultiCurrency\Interfaces; defined( 'ABSPATH' ) || exit; interface MultiCurrencySettingsInterface { /** * Checks if dev mode is enabled. * * @return bool */ public function is_dev_mode(): bool; /** * Gets the plugin file path. * * @return string */ public function get_plugin_file_path(): string; /** * Gets the plugin version. * * @return string */ public function get_plugin_version(): string; } includes/multi-currency/Interfaces/MultiCurrencyLocalizationInterface.php 0000777 00000001332 15251156605 0023143 0 ustar 00 <?php /** * Interface MultiCurrencyLocalizationInterface * * @package WooCommerce\Payments\MultiCurrency\Interfaces */ namespace WCPay\MultiCurrency\Interfaces; defined( 'ABSPATH' ) || exit; interface MultiCurrencyLocalizationInterface { /** * Retrieves the currency's format from mapped data. * * @param string $currency_code The currency code. * * @return array The currency's format. */ public function get_currency_format( $currency_code ): array; /** * Returns the locale data for a country. * * @param string $country Country code. * * @return array Array with the country's locale data. Empty array if country not found. */ public function get_country_locale_data( $country ): array; } includes/multi-currency/Interfaces/MultiCurrencyCacheInterface.php 0000777 00000003126 15251156605 0021521 0 ustar 00 <?php /** * Interface MultiCurrencyCacheInterface * * @package WooCommerce\Payments\MultiCurrency\Interfaces */ namespace WCPay\MultiCurrency\Interfaces; defined( 'ABSPATH' ) || exit; interface MultiCurrencyCacheInterface { const CURRENCIES_KEY = 'wcpay_multi_currency_cached_currencies'; /** * Gets a value from the cache. * * @param string $key The key to look for. * @param bool $force If set, return from the cache without checking for expiry. * * @return mixed The cache contents. */ public function get( string $key, bool $force = false ); /** * Gets a value from cache or regenerates and adds it to the cache. * * @param string $key The options key to cache the data under. * @param callable $generator Function/callable regenerating the missing value. If null or false is returned, it will be treated as an error. * @param callable $validate_data Function/callable validating the data after it is retrieved from the cache. If it returns false, the cache will be refreshed. * @param boolean $force_refresh Regenerates the cache regardless of its state if true. * @param boolean $refreshed Is set to true if the cache has been refreshed without errors and with a non-empty value. * * @return mixed|null The cache contents. NULL on failure */ public function get_or_add( string $key, callable $generator, callable $validate_data, bool $force_refresh = false, bool &$refreshed = false ); /** * Deletes a value from the cache. * * @param string $key The key to delete. * * @return void */ public function delete( string $key ); } includes/multi-currency/Logger.php 0000777 00000002336 15251156605 0013305 0 ustar 00 <?php /** * Class Logger * * @package WooCommerce\Payments\MultiCurrency */ namespace WCPay\MultiCurrency; /** * Logger for multi-currency. */ class Logger { /** * Log source identifier. */ const LOG_FILE = 'woopayments-multi-currency'; /** * The WooCommerce logger instance. * * @var \WC_Logger|null */ private $logger; /** * Log a debug message. * * @param string $message The message to log. */ public static function debug( $message ) { self::log( 'debug', $message ); } /** * Log an error message. * * @param string $message The message to log. */ public static function error( $message ) { self::log( 'error', $message ); } /** * Log a notice message. * * @param string $message The message to log. */ public static function notice( $message ) { self::log( 'notice', $message ); } /** * Log a message with a specific level. * * @param string $level The log level (debug, error, notice, etc.). * @param string $message The message to log. */ private static function log( $level, $message ) { if ( ! function_exists( 'wc_get_logger' ) ) { return; } $logger = wc_get_logger(); $logger->log( $level, $message, [ 'source' => self::LOG_FILE ] ); } } includes/multi-currency/Analytics.php 0000777 00000055613 15251156605 0014023 0 ustar 00 <?php /** * Class Compatibility * * @package WooCommerce\Payments\Compatibility */ namespace WCPay\MultiCurrency; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Utilities\OrderUtil; use WC_Order; use WC_Order_Refund; use WCPay\MultiCurrency\Interfaces\MultiCurrencySettingsInterface; defined( 'ABSPATH' ) || exit; /** * Class that contains Multi-Currency related support for WooCommerce analytics. */ class Analytics { const PRIORITY_EARLY = 1; const PRIORITY_DEFAULT = 10; const PRIORITY_LATE = 20; const PRIORITY_LATEST = 99999; const SCRIPT_NAME = 'WCPAY_MULTI_CURRENCY_ANALYTICS'; const SUPPORTED_CONTEXTS = [ 'orders', 'products', 'variations', 'categories', 'coupons', 'taxes' ]; /** * SQL string replacements made by the analytics Multi-Currency extension. * * @var array */ protected $sql_replacements = []; /** * Instance of MultiCurrency. * * @var MultiCurrency $multi_currency */ private $multi_currency; /** * Instance of MultiCurrencySettingsInterface. * * @var MultiCurrencySettingsInterface $settings_service */ private $settings_service; /** * Constructor * * @param MultiCurrency $multi_currency Instance of MultiCurrency. * @param MultiCurrencySettingsInterface $settings_service Instance of MultiCurrencySettingsInterface. */ public function __construct( MultiCurrency $multi_currency, MultiCurrencySettingsInterface $settings_service ) { $this->multi_currency = $multi_currency; $this->settings_service = $settings_service; $this->init(); } /** * Initialise all actions, filters and hooks related to analytics support. * * @return void */ public function init() { if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) { add_filter( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] ); $this->register_customer_currencies(); } if ( $this->settings_service->is_dev_mode() ) { add_filter( 'woocommerce_analytics_report_should_use_cache', [ $this, 'disable_report_caching' ] ); } // Add a filter when the order stats table is updated. add_filter( 'woocommerce_analytics_update_order_stats_data', [ $this, 'update_order_stats_data' ], self::PRIORITY_LATEST, 2 ); // Add filters when the query args are updated. add_filter( 'woocommerce_analytics_orders_query_args', [ $this, 'apply_customer_currency_args' ] ); add_filter( 'woocommerce_analytics_orders_stats_query_args', [ $this, 'apply_customer_currency_args' ] ); // If we aren't making a REST request, or no multi currency orders exist in the merchant's store, // return before adding these filters. if ( ! WC()->is_rest_api_request() || ! $this->has_multi_currency_orders() ) { return; } $this->set_sql_replacements(); // Add the filters that are applied in each analytics query. add_filter( 'woocommerce_analytics_clauses_select', [ $this, 'filter_select_clauses' ], self::PRIORITY_LATE, 2 ); add_filter( 'woocommerce_analytics_clauses_join', [ $this, 'filter_join_clauses' ], self::PRIORITY_LATE, 2 ); // Add the WHERE clause filter which is applied only on Order related queries. add_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this, 'filter_where_clauses' ] ); add_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $this, 'filter_where_clauses' ] ); add_filter( 'woocommerce_analytics_clauses_where_orders_stats_interval', [ $this, 'filter_where_clauses' ] ); // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! empty( $_GET['currency'] ) && $_GET['currency'] !== $this->multi_currency->get_default_currency()->get_code() ) { add_filter( 'woocommerce_analytics_clauses_select_orders_subquery', [ $this, 'filter_select_orders_clauses' ] ); add_filter( 'woocommerce_analytics_clauses_select_orders_stats_total', [ $this, 'filter_select_orders_clauses' ] ); } } /** * Register the CSS and JS scripts. * * @return void */ public function register_admin_scripts() { $this->multi_currency->register_script_with_dependencies( self::SCRIPT_NAME, 'dist/multi-currency-analytics' ); } /** * Add the list of currencies used on the store to the wcSettings to allow it to be accessed by the front-end JS script. * * @return void */ public function register_customer_currencies() { $data_registry = Package::container()->get( AssetDataRegistry::class ); if ( $data_registry->exists( 'customerCurrencies' ) ) { return; } $currencies = $this->multi_currency->get_all_customer_currencies(); $available_currencies = $this->multi_currency->get_available_currencies(); $currency_options = []; $default_currency = $this->multi_currency->get_default_currency(); // Add default currency to the list if it does not exist. if ( ! in_array( $default_currency->get_code(), $currencies, true ) ) { $currencies[] = $default_currency->get_code(); } foreach ( $currencies as $currency ) { if ( ! isset( $available_currencies[ $currency ] ) ) { continue; } $currency_details = $available_currencies[ $currency ]; $currency_options[] = [ 'label' => html_entity_decode( $currency_details->get_name() ), 'value' => $currency_details->get_code(), ]; } $data_registry->add( 'customerCurrencies', $currency_options ); } /** * Enqueue scripts used on the analytics WP Admin pages. * * @return void */ public function enqueue_admin_scripts() { $this->register_admin_scripts(); wp_enqueue_script( self::SCRIPT_NAME ); } /** * Disables report caching. Used for development of analytics related functionality. * To disable report caching * * @param array $args Filter arguments. * * @return boolean */ public function disable_report_caching( $args ): bool { return false; } /** * If the customer currency is set, add it as a query parameter to requests to the data store. * This ensures that the cache is updated when this value is changed between requests. * * @param array $args The arguments passed in via the GET request. * * @return array */ public function apply_customer_currency_args( $args ): array { $currency_args = $this->get_customer_currency_args_from_request(); return array_merge( $args, $currency_args ); } /** * When an order is updated in the stats table, perform a check to see if it is a Multi-Currency order * and convert the information into the store's default currency if it is. * * @param array $args - An array of the arguments to be inserted into the order stats table. * @param WC_Order $order - The order itself. * * @return array */ public function update_order_stats_data( array $args, $order ): array { if ( ! $this->should_convert_order_stats( $order ) ) { return $args; } $stripe_exchange_rate = $order->get_meta( '_wcpay_multi_currency_stripe_exchange_rate', true ) ? (float) $order->get_meta( '_wcpay_multi_currency_stripe_exchange_rate', true ) : null; $order_exchange_rate = ( 1 / (float) $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true ) ); $exchange_rate = $stripe_exchange_rate ?? $order_exchange_rate; $dp = wc_get_price_decimals(); $args['net_total'] = round( $this->convert_amount( (float) $args['net_total'], $exchange_rate ), $dp ); $args['shipping_total'] = round( $this->convert_amount( (float) $args['shipping_total'], $exchange_rate ), $dp ); $args['tax_total'] = round( $this->convert_amount( (float) $args['tax_total'], $exchange_rate ), $dp ); $args['total_sales'] = $args['net_total'] + $args['shipping_total'] + $args['tax_total']; return $args; } /** * Add columns to get the currency and converted amount (if required). * * @param string[] $clauses - An array containing the SELECT clauses to be applied. * @param string $context - The context in which this SELECT clause is being called. * * @return array */ public function filter_select_clauses( array $clauses, $context ): array { // If we are unable to identify a context, just return the clauses as is. if ( is_null( $context ) ) { return $clauses; } if ( apply_filters( MultiCurrency::FILTER_PREFIX . 'disable_filter_select_clauses', false ) ) { return $clauses; } $context_parts = explode( '_', $context ); $context_page = $context_parts[0] ?? 'generic'; $context_type = $context_parts[1] ?? null; // If we can't identify the type of context we are running in (stats or subquery), then return the clauses as is. if ( ! in_array( $context_type, [ 'stats', 'subquery' ], true ) ) { return $clauses; } $new_clauses = []; $sql_replacements = $this->get_sql_replacements(); foreach ( $clauses as $clause ) { if ( ! array_key_exists( $context_page, $sql_replacements ) ) { $replacements_array = $sql_replacements['generic'] ?? []; } else { $replacements_array = $sql_replacements[ $context_page ] ?? []; } foreach ( $replacements_array as $find => $replace ) { if ( strpos( $clause, $find ) !== false ) { $clause = str_replace( $find, $replace, $clause ); } } $new_clauses[] = $clause; } if ( $this->is_supported_context( $context ) && ( in_array( $context_page, self::SUPPORTED_CONTEXTS, true ) || $this->is_order_stats_table_used_in_clauses( $clauses ) ) ) { if ( $this->is_cot_enabled() ) { $new_clauses[] = ', wcpay_multicurrency_order_currency.currency AS order_currency'; } else { $new_clauses[] = ', wcpay_multicurrency_currency_meta.meta_value AS order_currency'; } $new_clauses[] = ', wcpay_multicurrency_default_currency_meta.meta_value AS order_default_currency'; $new_clauses[] = ', wcpay_multicurrency_exchange_rate_meta.meta_value AS exchange_rate'; $new_clauses[] = ', wcpay_multicurrency_stripe_exchange_rate_meta.meta_value AS stripe_exchange_rate'; } return apply_filters( MultiCurrency::FILTER_PREFIX . 'filter_select_clauses', $new_clauses ); } /** * Add a JOIN so that we can get the currency information. * * @param string[] $clauses - An array containing the JOIN clauses to be applied. * @param string $context - The context in which this SELECT clause is being called. * * @return array */ public function filter_join_clauses( array $clauses, $context ): array { global $wpdb; if ( apply_filters( MultiCurrency::FILTER_PREFIX . 'disable_filter_join_clauses', false ) ) { return $clauses; } $context_parts = explode( '_', $context, 2 ); $context_page = $context_parts[0] ?? 'generic'; $prefix = 'wcpay_multicurrency_'; $currency_tbl = $prefix . 'currency_meta'; $default_currency_tbl = $prefix . 'default_currency_meta'; $exchange_rate_tbl = $prefix . 'exchange_rate_meta'; $stripe_exchange_rate_tbl = $prefix . 'stripe_exchange_rate_meta'; // Allow this to work with custom order tables as well. if ( $this->is_cot_enabled() ) { $meta_table = $wpdb->prefix . 'wc_orders_meta'; $id_field = 'order_id'; $currency_tbl = $prefix . 'order_currency'; } else { $meta_table = $wpdb->postmeta; $id_field = 'post_id'; } // If this is a supported context, add the joins. If this is an unsupported context, see if we can add the joins. if ( $this->is_supported_context( $context ) && ( in_array( $context_page, self::SUPPORTED_CONTEXTS, true ) || $this->is_order_stats_table_used_in_clauses( $clauses ) ) ) { if ( $this->is_cot_enabled() ) { $clauses[] = "LEFT JOIN {$wpdb->prefix}wc_orders {$currency_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$currency_tbl}.id"; } else { $clauses[] = "LEFT JOIN {$meta_table} {$currency_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$currency_tbl}.{$id_field} AND {$currency_tbl}.meta_key = '_order_currency'"; } $clauses[] = "LEFT JOIN {$meta_table} {$default_currency_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$default_currency_tbl}.{$id_field} AND {$default_currency_tbl}.meta_key = '_wcpay_multi_currency_order_default_currency'"; $clauses[] = "LEFT JOIN {$meta_table} {$exchange_rate_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$exchange_rate_tbl}.{$id_field} AND {$exchange_rate_tbl}.meta_key = '_wcpay_multi_currency_order_exchange_rate'"; $clauses[] = "LEFT JOIN {$meta_table} {$stripe_exchange_rate_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$stripe_exchange_rate_tbl}.{$id_field} AND {$stripe_exchange_rate_tbl}.meta_key = '_wcpay_multi_currency_stripe_exchange_rate'"; } return apply_filters( MultiCurrency::FILTER_PREFIX . 'filter_join_clauses', $clauses ); } /** * Add the WHERE clauses (if a customer currency has been selected). * * @param string[] $clauses - An array containing the JOIN clauses to be applied. * * @return array */ public function filter_where_clauses( array $clauses ): array { if ( apply_filters( MultiCurrency::FILTER_PREFIX . 'disable_filter_where_clauses', false ) ) { return $clauses; } $prefix = 'wcpay_multicurrency_'; if ( $this->is_cot_enabled() ) { $currency_field = $prefix . 'order_currency.currency'; } else { $currency_field = $prefix . 'currency_meta.meta_value'; } $currency_args = $this->get_customer_currency_args_from_request(); if ( ! empty( $currency_args['currency_is'] ) ) { $currency_is = sprintf( "'%s'", implode( "', '", array_map( 'esc_sql', $currency_args['currency_is'] ) ) ); $clauses[] = "AND {$currency_field} IN ({$currency_is})"; } if ( ! empty( $currency_args['currency_is_not'] ) ) { $currency_is_not = sprintf( "'%s'", implode( "', '", array_map( 'esc_sql', $currency_args['currency_is_not'] ) ) ); $clauses[] = "AND {$currency_field} NOT IN ({$currency_is_not})"; } if ( ! empty( $currency_args['currency'] ) ) { global $wpdb; $expression = "AND {$currency_field} = '%s'"; $clauses[] = $wpdb->prepare( $expression, $currency_args['currency'] ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } return apply_filters( MultiCurrency::FILTER_PREFIX . 'filter_where_clauses', $clauses ); } /** * Convert amounts back to order currency (if a currency has been selected). * Skipping it for default currency. * * @param string[] $clauses - An array containing the SELECT orders clauses to be applied. * @return array */ public function filter_select_orders_clauses( array $clauses ): array { if ( apply_filters( MultiCurrency::FILTER_PREFIX . 'disable_filter_select_orders_clauses', false ) ) { return $clauses; } global $wpdb; $exchange_rate = 'wcpay_multicurrency_exchange_rate_meta.meta_value'; $stripe_exchange_rate = 'wcpay_multicurrency_stripe_exchange_rate_meta.meta_value'; $net_total = "{$wpdb->prefix}wc_order_stats.net_total"; foreach ( $clauses as $k => $clause ) { if ( strpos( $clause, $net_total ) !== false ) { $is_orders_subquery = strpos( $clause, $net_total . ',' ) !== false; $variable = $is_orders_subquery ? "$net_total," : $net_total; $alias = $is_orders_subquery ? ' as net_total,' : ''; $dp = wc_get_price_decimals(); $clauses[ $k ] = str_replace( $variable, $this->generate_case_when( $stripe_exchange_rate, "ROUND($net_total / $stripe_exchange_rate, $dp)", "ROUND($net_total * $exchange_rate, $dp)" ) . $alias, $clause ); } } return apply_filters( MultiCurrency::FILTER_PREFIX . 'filter_select_orders_clauses', $clauses ); } /** * Check to see whether we should convert an order to store in the order stats table. * * @param WC_Order|WC_Order_Refund $order The order. * * @return boolean */ private function should_convert_order_stats( $order ): bool { $default_currency = $this->multi_currency->get_default_currency(); // If this order was in the default currency, or the meta information isn't set on the order, return false. if ( ! $order || $order->get_currency() === $default_currency->get_code() || ! $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true ) || $order->get_meta( '_wcpay_multi_currency_order_default_currency', true ) !== $default_currency->get_code() ) { return false; } return true; } /** * Convert an amount to the store's default currency in order to store in the stats table. * * @param float $amount The amount to convert into the store's default currency. * @param float $exchange_rate The exchange rate to use for the conversion. * * @return float The converted amount. */ private function convert_amount( float $amount, float $exchange_rate ): float { return $amount * $exchange_rate; } /** * Check whether the order stats table is referenced in the clauses, to work out whether * to add the JOIN columns for Multi-Currency. * * @param array $clauses The array containing the clauses used. * * @return boolean Whether the order stats table is referenced. */ private function is_order_stats_table_used_in_clauses( array $clauses ): bool { global $wpdb; foreach ( $clauses as $clause ) { if ( strpos( $clause, "{$wpdb->prefix}wc_order_stats" ) !== false ) { return true; } } return false; } /** * There are some queries which are made in the analytics which are actually sub-queries * which are used to join on an individual item/coupon/tax code. In these cases, rather than * the context being the expected format e.g. product_stats_total, it will simply be 'product'. * In these cases, we don't want to add the join columns or select them. * * @param string $context The context the query was made in. * * @return boolean */ private function is_supported_context( string $context ): bool { $unsupported_contexts = [ 'products', 'coupons', 'taxes', 'variations', 'categories' ]; if ( in_array( $context, $unsupported_contexts, true ) ) { return false; } return true; } /** * Generate a case when statement using the provided variables. * * @param string $variable The SQL variable we want to check for NULL. * @param string $then The THEN clause. * @param string $else The ELSE clause. * * @return string */ private function generate_case_when( string $variable, string $then, string $else ): string { return "CASE WHEN {$variable} IS NOT NULL THEN {$then} ELSE {$else} END"; } /** * Perform an SQL query to determine whether Multi Currency has ever been used on this store, * by checking how many orders are in the database where an exchange currency rate has been stored. * * @return bool */ private function has_multi_currency_orders() { global $wpdb; // Using full SQL instead of variables to keep WPCS happy. if ( $this->is_cot_enabled() ) { $result = $wpdb->get_var( "SELECT EXISTS( SELECT 1 FROM {$wpdb->prefix}wc_orders_meta WHERE meta_key = '_wcpay_multi_currency_order_exchange_rate' LIMIT 1) AS count;" ); } else { $result = $wpdb->get_var( "SELECT EXISTS( SELECT 1 FROM {$wpdb->postmeta} WHERE meta_key = '_wcpay_multi_currency_order_exchange_rate' LIMIT 1) AS count;" ); } return intval( $result ) === 1; } /** * Get the SQL replacements variable. * * @return array */ private function get_sql_replacements(): array { return $this->sql_replacements; } /** * Check the passed in query params to see if currency has been passed in. * Will return null if no currency variable was passed in, otherwise will * return the currency. * * @return array */ private function get_customer_currency_args_from_request(): array { $args = [ 'currency_is' => [], 'currency_is_not' => [], 'currency' => null, ]; /* phpcs:disable WordPress.Security.NonceVerification */ if ( isset( $_GET['currency_is'] ) && is_array( $_GET['currency_is'] ) ) { $args['currency_is'] = array_map( 'sanitize_text_field', wp_unslash( $_GET['currency_is'] ) ); } if ( isset( $_GET['currency_is_not'] ) ) { $args['currency_is_not'] = array_map( 'sanitize_text_field', wp_unslash( $_GET['currency_is_not'] ) ); } if ( isset( $_GET['currency'] ) ) { $args['currency'] = sanitize_text_field( wp_unslash( $_GET['currency'] ) ); } /* phpcs:enable WordPress.Security.NonceVerification */ return $args; } /** * Set the SQL replacements variable. * * @return void */ private function set_sql_replacements() { $default_currency = 'wcpay_multicurrency_default_currency_meta.meta_value'; $exchange_rate = 'wcpay_multicurrency_exchange_rate_meta.meta_value'; $stripe_exchange_rate = 'wcpay_multicurrency_stripe_exchange_rate_meta.meta_value'; $discount_amount = $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(discount_amount * {$stripe_exchange_rate}, 2)", "ROUND(discount_amount * (1 / {$exchange_rate} ), 2)" ), 'discount_amount' ); $product_net_revenue = $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(product_net_revenue * {$stripe_exchange_rate}, 2)", "ROUND(product_net_revenue * (1 / {$exchange_rate} ), 2)" ), 'product_net_revenue' ); $product_gross_revenue = $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(product_gross_revenue * {$stripe_exchange_rate}, 2)", "ROUND(product_gross_revenue * (1 / {$exchange_rate} ), 2)" ), 'product_gross_revenue' ); $this->sql_replacements = [ 'generic' => [ 'discount_amount' => $discount_amount, 'product_net_revenue' => $product_net_revenue, 'product_gross_revenue' => $product_gross_revenue, ], 'orders' => [ 'discount_amount' => $discount_amount, ], 'products' => [ 'product_net_revenue' => $product_net_revenue, 'product_gross_revenue' => $product_gross_revenue, ], 'variations' => [ 'product_net_revenue' => $product_net_revenue, 'product_gross_revenue' => $product_gross_revenue, ], 'categories' => [ 'product_net_revenue' => $product_net_revenue, 'product_gross_revenue' => $product_gross_revenue, ], 'taxes' => [ 'SUM(total_tax)' => 'SUM(' . $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(total_tax * {$stripe_exchange_rate}, 2)", "ROUND(total_tax * (1 / {$exchange_rate} ), 2)" ), 'total_tax' ) . ')', 'SUM(order_tax)' => 'SUM(' . $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(order_tax * {$stripe_exchange_rate}, 2)", "ROUND(order_tax * (1 / {$exchange_rate} ), 2)" ), 'order_tax' ) . ')', 'SUM(shipping_tax)' => 'SUM(' . $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(shipping_tax * {$stripe_exchange_rate}, 2)", "ROUND(shipping_tax * (1 / {$exchange_rate} ), 2)" ), 'shipping_tax' ) . ')', ], 'coupons' => [ 'discount_amount' => $discount_amount, ], ]; } /** * Checks whether Custom Order Tables are enabled. * * @return bool */ private function is_cot_enabled(): bool { return class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled(); } } includes/class-wc-payments-explicit-price-formatter.php 0000777 00000012654 15251156605 0017402 0 ustar 00 <?php /** * Class WC_Payments_Explicit_Price_Formatter * * @package WooCommerce\Payments */ use WCPay\MultiCurrency\MultiCurrency; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Class for displaying the explicit prices on total amounts. * * Also used for consistent rendering of price values with currency (get_explicit_price_with_currency()). */ class WC_Payments_Explicit_Price_Formatter { /** * The Multi-Currency instance for checking the number of enabled currencies * * @var MultiCurrency */ private static $multi_currency_instance = null; /** * Inits the formatter, registering the necessary hooks. */ public static function init() { add_filter( 'woocommerce_cart_total', [ __CLASS__, 'get_explicit_price' ], 100 ); add_filter( 'woocommerce_get_formatted_order_total', [ __CLASS__, 'get_explicit_price' ], 100, 2 ); add_action( 'woocommerce_admin_order_totals_after_tax', [ __CLASS__, 'register_formatted_woocommerce_price_filter' ] ); add_action( 'woocommerce_admin_order_totals_after_total', [ __CLASS__, 'unregister_formatted_woocommerce_price_filter' ] ); } /** * Overrides the MultiCurrency instance that the class uses. * Mostly for testing purposes. * * @param MultiCurrency $multi_currency MultCurrency class. * * @return void */ public static function set_multi_currency_instance( MultiCurrency $multi_currency ) { self::$multi_currency_instance = $multi_currency; } /** * Checks if the method should output explicit price * * @return bool Whether if it should return explicit price or not */ public static function should_output_explicit_price() { // If customer Multi-Currency is disabled, don't use explicit currencies. // Because it'll have only the store currency active, same as count == 1. if ( ! WC_Payments_Features::is_customer_multi_currency_enabled() ) { return false; } // If the MultiCurrency instance hasn't been defined yet, fetch the instance. if ( null === self::$multi_currency_instance ) { self::$multi_currency_instance = WC_Payments_Multi_Currency(); } // If the instance isn't initialized yet, skip the checks. if ( ! self::$multi_currency_instance->is_initialized() ) { return false; } // If no additional currencies are enabled, skip it. if ( ! self::$multi_currency_instance->has_additional_currencies_enabled() ) { return false; } return true; } /** * Registers the get_explicit_price filter for the order details screen. * * There are no hooks that enable us to filter the output on the order details screen. * So, we need to add a filter to formatted_woocommerce_price. We use specific actions * to register and unregister the filter, so that only the appropriate prices are affected. */ public static function register_formatted_woocommerce_price_filter() { add_filter( 'wc_price_args', [ __CLASS__, 'get_explicit_price_args' ], 100 ); } /** * Unregisters the get_explicit_price filter for the order details screen. * * There are no hooks that enable us to filter the output on the order details screen. * So, we need to add a filter to formatted_woocommerce_price. We use specific actions * to register and unregister the filter, so that only the appropriate prices are affected. */ public static function unregister_formatted_woocommerce_price_filter() { remove_filter( 'wc_price_args', [ __CLASS__, 'get_explicit_price_args' ], 100 ); } /** * Returns the price suffixed with the appropriate currency code, if not already. * * @param string $price The price. * @param WC_Abstract_Order|null $order The order. * * @return string */ public static function get_explicit_price( string $price, ?WC_Abstract_Order $order = null ) { if ( null === $order ) { $currency_code = get_woocommerce_currency(); } else { $currency_code = $order->get_currency(); } return static::get_explicit_price_with_currency( $price, $currency_code ); } /** * Returns a formatted price string suffixed with the appropriate currency code (if necessary). * * In multi-currency stores, order and price values are rendered with currency suffix. * This method only renders the currency suffix if appropriate (see `should_output_explicit_price`). * * @param string $price A price value (as a string). * @param ?string $currency_code Currency of the price. * * @return string Price value with currency code suffix if necessary. */ public static function get_explicit_price_with_currency( string $price, ?string $currency_code ) { if ( false === static::should_output_explicit_price() ) { return $price; } if ( empty( $currency_code ) ) { return $price; } $price_to_check = html_entity_decode( wp_strip_all_tags( $price ), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ); if ( false === strpos( $price_to_check, trim( $currency_code ) ) ) { return $price . ' ' . $currency_code; } return $price; } /** * Alters the price formatting arguments to include explicit format * * @param array $args Price formatting args passed through `wc_price_args` filter. * * @return array The modified arguments */ public static function get_explicit_price_args( $args ) { if ( false === static::should_output_explicit_price() ) { return $args; } if ( false === strpos( $args['price_format'], $args['currency'] ) ) { $args['price_format'] = sprintf( '%s %s', $args['price_format'], $args['currency'] ); } return $args; } } includes/class-wc-payments-checkout.php 0000777 00000052154 15251156605 0014264 0 ustar 00 <?php /** * Class WC_Payments_Checkout * * @package WooCommerce\Payments */ namespace WCPay; use Exception; use Jetpack_Options; use WC_AJAX; use WC_Checkout; use WC_Payments; use WC_Payments_Account; use WC_Payments_Customer_Service; use WC_Payments_Fraud_Service; use WC_Payments_Utils; use WC_Payments_Features; use WCPay\Constants\Payment_Method; use WCPay\Fraud_Prevention\Fraud_Prevention_Service; use WC_Payment_Gateway_WCPay; use WCPay\WooPay\WooPay_Utilities; use WCPay\Payment_Methods\UPE_Payment_Method; use WCPay\WooPay\WooPay_Session; /** * WC_Payments_Checkout */ class WC_Payments_Checkout { /** * WC Payments Gateway. * * @var WC_Payment_Gateway_WCPay */ protected $gateway; /** * WooPay Utilities. * * @var WooPay_Utilities */ protected $woopay_util; /** * WC Payments Account. * * @var WC_Payments_Account */ protected $account; /** * WC Payments Customer Service * * @var WC_Payments_Customer_Service */ protected $customer_service; /** * WC_Payments_Fraud_Service instance to get information about fraud services. * * @var WC_Payments_Fraud_Service */ protected $fraud_service; /** * Construct. * * @param WC_Payment_Gateway_WCPay $gateway WC Payment Gateway. * @param WooPay_Utilities $woopay_util WooPay Utilities. * @param WC_Payments_Account $account WC Payments Account. * @param WC_Payments_Customer_Service $customer_service WC Payments Customer Service. * @param WC_Payments_Fraud_Service $fraud_service Fraud service instance. */ public function __construct( WC_Payment_Gateway_WCPay $gateway, WooPay_Utilities $woopay_util, WC_Payments_Account $account, WC_Payments_Customer_Service $customer_service, WC_Payments_Fraud_Service $fraud_service ) { $this->gateway = $gateway; $this->woopay_util = $woopay_util; $this->account = $account; $this->customer_service = $customer_service; $this->fraud_service = $fraud_service; } /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_action( 'wc_payments_set_gateway', [ $this, 'set_gateway' ] ); add_action( 'wc_payments_add_upe_payment_fields', [ $this, 'payment_fields' ] ); add_action( 'wp', [ $this->gateway, 'maybe_process_upe_redirect' ] ); add_action( 'wp_ajax_save_upe_appearance', [ $this->gateway, 'save_upe_appearance_ajax' ] ); add_action( 'wp_ajax_nopriv_save_upe_appearance', [ $this->gateway, 'save_upe_appearance_ajax' ] ); add_action( 'switch_theme', [ $this->gateway, 'clear_upe_appearance_transient' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ $this->gateway, 'clear_upe_appearance_transient' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts_for_zero_order_total' ], 11 ); add_action( 'woocommerce_after_checkout_form', [ $this, 'maybe_load_checkout_scripts' ] ); } /** * Registers all scripts, necessary for the gateway. */ public function register_scripts() { if ( wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) { return; } // Register Stripe's JavaScript using the same ID as the Stripe Gateway plugin. This prevents this JS being // loaded twice in the event a site has both plugins enabled. We still run the risk of different plugins // loading different versions however. If Stripe release a v4 of their JavaScript, we could consider // changing the ID to stripe_v4. This would allow older plugins to keep using v3 while we used any new // feature in v4. Stripe have allowed loading of 2 different versions of stripe.js in the past ( // https://stripe.com/docs/stripe-js/elements/migrating). wp_register_script( 'stripe', 'https://js.stripe.com/v3/', [], '3.0', true ); $script_dependencies = [ 'stripe', 'wc-checkout', 'wp-i18n' ]; if ( $this->gateway->supports( 'tokenization' ) ) { $script_dependencies[] = 'woocommerce-tokenization-form'; } Fraud_Prevention_Service::maybe_append_fraud_prevention_token(); WC_Payments::register_script_with_dependencies( 'wcpay-upe-checkout', 'dist/checkout', $script_dependencies ); } /** * Registers scripts necessary for the gateway, even when cart order total is 0. * This is done so that if the cart is modified via AJAX on checkout, * the scripts are still loaded. */ public function register_scripts_for_zero_order_total() { if ( isset( WC()->cart ) && ! WC()->cart->is_empty() && ! WC()->cart->needs_payment() && is_checkout() && ! has_block( 'woocommerce/checkout' ) && ! wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) { $this->load_checkout_scripts(); } } /** * Sometimes the filters can remove the payment gateway from the checkout page which results in the payment fields not being displayed. * This could prevent loading of the payment fields (checkout) scripts. * This function ensures that these scripts are loaded. */ public function maybe_load_checkout_scripts() { if ( is_checkout() && ! wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) { $this->load_checkout_scripts(); } } /** * Generates the configuration values, needed for payment fields. * * Isolated as a separate method in order to be available both * during the classic checkout, as well as the checkout block. * * @return array */ public function get_payment_fields_js_config() { // Needed to init the hooks. WC_Checkout::instance(); // The registered card gateway is more reliable than $this->gateway, but if it isn't available for any reason, fall back to the gateway provided to this checkout class. $gateway = WC_Payments::get_gateway() ?? $this->gateway; $js_config = [ 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ), 'testMode' => WC_Payments::mode()->is_test(), 'accountId' => $this->account->get_stripe_account_id(), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'createSetupIntentNonce' => wp_create_nonce( 'wcpay_create_setup_intent_nonce' ), 'initWooPayNonce' => wp_create_nonce( 'wcpay_init_woopay_nonce' ), 'saveUPEAppearanceNonce' => wp_create_nonce( 'wcpay_save_upe_appearance_nonce' ), 'genericErrorMessage' => __( 'There was a problem processing the payment. Please check your email inbox and refresh the page to try again.', 'woocommerce-payments' ), 'fraudServices' => $this->fraud_service->get_fraud_services_config(), 'features' => $this->gateway->supports, 'forceNetworkSavedCards' => WC_Payments::is_network_saved_cards_enabled() || $gateway->should_use_stripe_platform_on_checkout_page(), 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), 'isPreview' => is_preview(), 'isSavedCardsEnabled' => $this->gateway->is_saved_cards_enabled(), 'isWooPayEnabled' => $this->woopay_util->should_enable_woopay( $this->gateway ) && $this->woopay_util->should_enable_woopay_on_guest_checkout(), 'isWoopayExpressCheckoutEnabled' => $this->woopay_util->is_woopay_express_checkout_enabled(), 'isWoopayFirstPartyAuthEnabled' => $this->woopay_util->is_woopay_first_party_auth_enabled(), 'isWooPayEmailInputEnabled' => $this->woopay_util->is_woopay_email_input_enabled(), 'isWooPayDirectCheckoutEnabled' => WC_Payments_Features::is_woopay_direct_checkout_enabled(), 'isWooPayGlobalThemeSupportEnabled' => $this->gateway->is_woopay_global_theme_support_enabled(), 'woopayHost' => WooPay_Utilities::get_woopay_url(), 'platformTrackerNonce' => wp_create_nonce( 'platform_tracks_nonce' ), 'accountIdForIntentConfirmation' => apply_filters( 'wc_payments_account_id_for_intent_confirmation', '' ), 'wcpayVersionNumber' => WCPAY_VERSION_NUMBER, 'woopaySignatureNonce' => wp_create_nonce( 'woopay_signature_nonce' ), 'woopaySessionNonce' => wp_create_nonce( 'woopay_session_nonce' ), 'woopayMerchantId' => Jetpack_Options::get_option( 'id' ), 'icon' => $this->gateway->get_icon_url(), 'woopayMinimumSessionData' => WooPay_Session::get_woopay_minimum_session_data(), ]; $payment_fields = $js_config; $payment_fields['gatewayId'] = WC_Payment_Gateway_WCPay::GATEWAY_ID; $payment_fields['isCheckout'] = is_checkout(); $payment_fields['paymentMethodsConfig'] = $this->get_enabled_payment_method_config(); $payment_fields['testMode'] = WC_Payments::mode()->is_test(); $payment_fields['upeAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_APPEARANCE_TRANSIENT ); $payment_fields['upeAddPaymentMethodAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_ADD_PAYMENT_METHOD_APPEARANCE_TRANSIENT ); $payment_fields['upeBnplProductPageAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_PRODUCT_PAGE_APPEARANCE_TRANSIENT ); $payment_fields['upeBnplClassicCartAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CLASSIC_CART_APPEARANCE_TRANSIENT ); $payment_fields['upeBnplCartBlockAppearance'] = get_transient( WC_Payment_Gateway_WCPay::UPE_BNPL_CART_BLOCK_APPEARANCE_TRANSIENT ); $payment_fields['wcBlocksUPEAppearance'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT ); $payment_fields['wcBlocksUPEAppearanceTheme'] = get_transient( WC_Payment_Gateway_WCPay::WC_BLOCKS_UPE_APPEARANCE_THEME_TRANSIENT ); $payment_fields['cartContainsSubscription'] = $this->gateway->is_subscription_item_in_cart(); $payment_fields['currency'] = get_woocommerce_currency(); $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); $payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() ); $enabled_billing_fields = []; foreach ( WC()->checkout()->get_checkout_fields( 'billing' ) as $billing_field => $billing_field_options ) { if ( ! isset( $billing_field_options['enabled'] ) || $billing_field_options['enabled'] ) { $enabled_billing_fields[ $billing_field ] = [ 'required' => $billing_field_options['required'] ?? false, ]; } } $payment_fields['enabledBillingFields'] = $enabled_billing_fields; if ( is_wc_endpoint_url( 'order-pay' ) ) { if ( $this->gateway->is_subscriptions_enabled() && $this->gateway->is_changing_payment_method_for_subscription() ) { $payment_fields['isChangingPayment'] = true; return $payment_fields; // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in. } $order_id = absint( get_query_var( 'order-pay' ) ); $order = wc_get_order( $order_id ); if ( is_a( $order, 'WC_Order' ) && current_user_can( 'pay_for_order', $order->get_id() ) ) { $payment_fields['isOrderPay'] = true; $payment_fields['orderId'] = $order_id; $order_currency = $order->get_currency(); $payment_fields['currency'] = $order_currency; $payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $order->get_total(), $order_currency ); } } // Get the store base country. $payment_fields['storeCountry'] = WC()->countries->get_base_country(); /** * Allows filtering of the JS config for the payment fields. * * @param array $js_config The JS config for the payment fields. */ return apply_filters( 'wcpay_payment_fields_js_config', $payment_fields ); // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in. } /** * Gets payment method settings to pass to client scripts * * @return array */ public function get_enabled_payment_method_config() { $settings = []; $enabled_payment_methods = $this->gateway->get_payment_method_ids_enabled_at_checkout(); foreach ( $enabled_payment_methods as $payment_method_id ) { // Link by Stripe should be validated with available fees. if ( Payment_Method::LINK === $payment_method_id ) { if ( ! in_array( Payment_Method::LINK, array_keys( $this->account->get_fees() ), true ) ) { continue; } } $settings[ $payment_method_id ] = $this->get_config_for_payment_method( $payment_method_id, $this->account->get_account_country() ); } return $settings; } /** * Gets the config for all payment methods. * * @return array */ public function get_all_payment_method_config() { $payment_method_configs = []; // Get all the registered payment methods. $registered_payment_methods = array_keys( $this->gateway->get_payment_methods() ); foreach ( $registered_payment_methods as $payment_method_id ) { $payment_method_configs[ $payment_method_id ] = $this->get_config_for_payment_method( $payment_method_id, $this->account->get_account_country() ); } return $payment_method_configs; } /** * Gets the config for a payment method. * * @param string $payment_method_id The payment method ID. * @param string $account_country The account country. * @return array */ private function get_config_for_payment_method( $payment_method_id, $account_country ) { $payment_method = $this->gateway->wc_payments_get_payment_method_by_id( $payment_method_id ); if ( ! $payment_method ) { return []; } $config = [ 'isReusable' => $payment_method->is_reusable(), 'isBnpl' => $payment_method->is_bnpl(), 'title' => $payment_method->get_title( $account_country ), 'icon' => $payment_method->get_icon( $account_country ), 'darkIcon' => $payment_method->get_dark_icon( $account_country ), 'showSaveOption' => $this->should_upe_payment_method_show_save_option( $payment_method ), 'countries' => $payment_method->get_countries(), ]; $gateway_for_payment_method = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id ); $config['gatewayId'] = $gateway_for_payment_method->id; $config['testingInstructions'] = WC_Payments_Utils::esc_interpolated_html( /* translators: link to Stripe testing page */ $payment_method->get_testing_instructions( $account_country ), [ 'a' => '<a href="https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#test-cards" target="_blank">', 'strong' => '<strong>', 'number' => '<button type="button" class="js-woopayments-copy-test-number" aria-label="' . esc_attr( __( 'Click to copy the test number to clipboard', 'woocommerce-payments' ) ) . '" title="' . esc_attr( __( 'Copy to clipboard', 'woocommerce-payments' ) ) . '"><i></i><span>', ] ); $should_enable_network_saved_cards = Payment_Method::CARD === $payment_method_id && WC_Payments::is_network_saved_cards_enabled(); $config['forceNetworkSavedCards'] = $should_enable_network_saved_cards || $gateway_for_payment_method->should_use_stripe_platform_on_checkout_page(); return $config; } /** * Checks if the save option for a payment method should be displayed or not. * * @param UPE_Payment_Method $payment_method UPE Payment Method instance. * @return bool - True if the payment method is reusable and the saved cards feature is enabled for the gateway and there is no subscription item in the cart, false otherwise. */ private function should_upe_payment_method_show_save_option( $payment_method ) { if ( $payment_method->get_id() === Payment_Method::CARD && is_user_logged_in() && WC_Payments_Features::is_woopay_enabled() ) { return false; } if ( $payment_method->is_reusable() ) { return $this->gateway->is_saved_cards_enabled() && ! $this->gateway->is_subscription_item_in_cart(); } return false; } /** * Renders the UPE input fields needed to get the user's payment information on the checkout page. * * We also add the JavaScript which drives the UI. */ public function payment_fields() { try { $display_tokenization = $this->gateway->supports( 'tokenization' ) && ( is_checkout() || is_add_payment_method_page() ); /** * Localizing scripts within shortcodes does not work in WP 5.9, * but we need `$this->get_payment_fields_js_config` to be called * before `$this->saved_payment_methods()`. */ if ( ! wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) { $payment_fields = $this->get_payment_fields_js_config(); wp_enqueue_script( 'wcpay-upe-checkout' ); /** * We can't localize the script right away since at this point is not registered yet. * We also need to make sure it that it only runs once (using a dummy action), even if * there are multiple payment methods available; otherwise the data will be overwritten * which is pointless. * * The same applies for `wcpayCustomerData` a few lines below. */ add_action( 'wp_footer', function () use ( $payment_fields ) { if ( ! did_action( '__wcpay_upe_config_localized' ) ) { wp_localize_script( 'wcpay-upe-checkout', 'wcpay_upe_config', $payment_fields ); } do_action( '__wcpay_upe_config_localized' ); } ); $prepared_customer_data = $this->customer_service->get_prepared_customer_data(); if ( ! empty( $prepared_customer_data ) ) { add_action( 'wp_footer', function () use ( $prepared_customer_data ) { if ( ! did_action( '__wcpay_customer_data_localized' ) ) { wp_localize_script( 'wcpay-upe-checkout', 'wcpayCustomerData', $prepared_customer_data ); } do_action( '__wcpay_customer_data_localized' ); } ); } WC_Payments_Utils::enqueue_style( 'wcpay-upe-checkout', plugins_url( 'dist/checkout.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'dist/checkout.css' ), 'all' ); } ?> <div class="wcpay-upe-form" data-payment-method-type="<?php echo esc_attr( $this->gateway->get_stripe_id() ); ?>" > <?php // Output the form HTML. if ( ! empty( $this->gateway->get_description() ) ) : ?> <p><?php echo wp_kses_post( $this->gateway->get_description() ); ?></p> <?php endif; if ( WC_Payments::mode()->is_test() && false !== $this->gateway->get_payment_method()->get_testing_instructions( $this->account->get_account_country() ) ) : ?> <p class="testmode-info"> <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo WC_Payments_Utils::esc_interpolated_html( /* translators: link to Stripe testing page */ $this->gateway->get_payment_method()->get_testing_instructions( $this->account->get_account_country() ), [ 'a' => '<a href="https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#test-cards" target="_blank">', 'strong' => '<strong>', 'number' => '<button type="button" class="js-woopayments-copy-test-number" aria-label="' . esc_attr( __( 'Click to copy the test number to clipboard', 'woocommerce-payments' ) ) . '" title="' . esc_attr( __( 'Copy to clipboard', 'woocommerce-payments' ) ) . '"><i></i><span>', ] ); ?> </p> <?php endif; if ( $display_tokenization ) { $this->gateway->tokenization_script(); // avoid showing saved payment methods on my-accounts add payment method page. if ( ! is_add_payment_method_page() ) { $this->gateway->saved_payment_methods(); } } ?> <fieldset style="padding: 7px" class="wc-payment-form"> <?php $this->gateway->display_gateway_html(); if ( $this->gateway->is_saved_cards_enabled() && $this->gateway->should_support_saved_payments() ) { $force_save_payment = ( $display_tokenization && ! apply_filters( 'wc_payments_display_save_payment_method_checkbox', $display_tokenization ) ) || is_add_payment_method_page(); if ( is_user_logged_in() || $force_save_payment ) { $this->gateway->save_payment_method_checkbox( $force_save_payment ); } } ?> </fieldset> </div> <?php do_action( 'wcpay_payment_fields_upe', $this->gateway->id ); } catch ( \Exception $e ) { // Output the error message. Logger::log( 'Error: ' . $e->getMessage() ); ?> <div> <?php echo esc_html__( 'An error was encountered when preparing the payment form. Please try again later.', 'woocommerce-payments' ); ?> </div> <?php } } /** * Set gateway * * @param string $payment_method_id Payment method ID. * @return void */ public function set_gateway( $payment_method_id ) { if ( null !== $payment_method_id ) { $this->gateway = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id ); } } /** * Load the checkout scripts. */ private function load_checkout_scripts() { WC_Payments::get_gateway()->tokenization_script(); $script_handle = 'wcpay-upe-checkout'; $js_object = 'wcpay_upe_config'; wp_localize_script( $script_handle, $js_object, WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() ); wp_enqueue_script( $script_handle ); } } includes/reports/class-wc-rest-payments-reports-transactions-controller.php 0000777 00000032453 15251156605 0023515 0 ustar 00 <?php /** * Class WC_REST_Payments_Reports_Transactions_Controller * * @package WooCommerce\Payments\Reports */ use WCPay\Core\Server\Request\List_Transactions; defined( 'ABSPATH' ) || exit; /** * REST controller for the transaction reports. */ class WC_REST_Payments_Reports_Transactions_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/reports/transactions'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_transactions' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => $this->get_collection_params(), ], 'schema' => [ $this, 'get_item_schema' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>\w+)', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_transaction' ], 'permission_callback' => [ $this, 'check_permission' ], ], 'schema' => [ $this, 'get_item_schema' ], ] ); } /** * Retrieve transactions to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_transactions( $request ) { $wcpay_request = List_Transactions::from_rest_request( $request ); $wcpay_request->set_page_size( $request->get_param( 'per_page' ) ?? 25 ); // Setting the request with filters. $request->get_param( 'type' ) ? $wcpay_request->set_type_is( $request->get_param( 'type' ) ) : null; $filters = [ 'order_id_is' => $request->get_param( 'order_id' ), 'customer_email_is' => $request->get_param( 'customer_email' ), 'source_is' => $request->get_param( 'payment_method_type' ), ]; $wcpay_request->set_filters( $filters ); $response = $wcpay_request->handle_rest_request(); if ( is_wp_error( $response ) ) { return $response; } $data = []; foreach ( $response['data'] ?? [] as $transaction ) { $response = $this->prepare_item_for_response( $transaction, $request ); $data[] = $this->prepare_response_for_collection( $response ); } return rest_ensure_response( $data ); } /** * Retrieve transaction to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_transaction( $request ) { $wcpay_request = List_Transactions::create(); $wcpay_request->set_filters( [ 'transaction_id_is' => $request->get_param( 'id' ) ] ); $wcpay_request->set_sort_by( 'date' ); // Default sort. $wcpay_request->set_page_size( 1 ); // Set page size to limit to only one record. $response = $wcpay_request->handle_rest_request(); if ( is_wp_error( $response ) ) { return $response; } $transaction = $response['data'][0] ?? null; if ( ! $transaction ) { return rest_ensure_response( [] ); } $response = $this->prepare_item_for_response( $transaction, $request ); $response = rest_ensure_response( $this->prepare_response_for_collection( $response ) ); return $response; } /** * Prepare each item for response. * * @param array|mixed $item Item to prepare. * @param WP_REST_Request $request Request instance. * * @return WP_REST_Response|WP_Error */ public function prepare_item_for_response( $item, $request ) { $prepared_item = []; $prepared_item['transaction_id'] = $item['transaction_id']; $prepared_item['date'] = $item['date']; $prepared_item['payment_id'] = $item['payment_intent_id']; $prepared_item['channel'] = $item['channel']; $prepared_item['payment_method'] = [ 'type' => $item['source'], ]; $prepared_item['type'] = $item['type']; $prepared_item['transaction_currency'] = $item['customer_currency']; $prepared_item['amount'] = $item['amount']; $prepared_item['exchange_rate'] = $item['exchange_rate']; $prepared_item['deposit_currency'] = $item['currency']; $prepared_item['fees'] = $item['fees']; $prepared_item['customer'] = [ 'name' => $item['customer_name'], 'email' => $item['customer_email'], 'country' => $item['customer_country'], ]; $prepared_item['net_amount'] = $item['net']; $prepared_item['order_id'] = $item['order_id']; $prepared_item['risk_level'] = $item['risk_level']; $prepared_item['deposit_date'] = $item['available_on']; $prepared_item['deposit_id'] = $item['deposit_id']; $prepared_item['deposit_status'] = $item['deposit_status'] ?? null; $context = $request['context'] ?? 'view'; $prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request ); $prepared_item = $this->filter_response_by_context( $prepared_item, $context ); return rest_ensure_response( $prepared_item ); } /** * Collection args params. * * @return array[] */ public function get_collection_params() { return [ 'date_before' => [ 'description' => __( 'Filter transactions before this date.', 'woocommerce-payments' ), 'type' => 'string', 'format' => 'date-time', 'required' => false, ], 'date_after' => [ 'description' => __( 'Filter transactions after this date.', 'woocommerce-payments' ), 'type' => 'string', 'format' => 'date-time', 'required' => false, ], 'date_between' => [ 'description' => __( 'Filter transactions between these dates.', 'woocommerce-payments' ), 'type' => 'array', ], 'order_id' => [ 'description' => __( 'Filter transactions based on the associated order ID.', 'woocommerce-payments' ), 'type' => 'integer', 'required' => false, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ], 'deposit_id' => [ 'description' => __( 'Filter transactions based on the associated deposit ID.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'validate_callback' => 'rest_validate_request_arg', ], 'customer_email' => [ 'description' => __( 'Filter transactions based on the customer email.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'validate_callback' => 'rest_validate_request_arg', ], 'payment_method_type' => [ 'description' => __( 'Filter transactions based on the payment method used.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'validate_callback' => 'rest_validate_request_arg', ], 'type' => [ 'description' => __( 'Filter transactions where type is a specific value.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'validate_callback' => 'rest_validate_request_arg', ], 'match' => [ 'description' => __( 'Match filter for the transactions.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, ], 'user_timezone' => [ 'description' => __( 'Include timezone into date filtering.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, ], 'page' => [ 'description' => __( 'Page number.', 'woocommerce-payments' ), 'type' => 'integer', 'required' => false, 'default' => 1, 'minimum' => 1, ], 'per_page' => [ 'description' => __( 'Page size.', 'woocommerce-payments' ), 'type' => 'integer', 'required' => false, 'default' => 25, 'minimum' => 1, 'maximum' => 100, ], 'sort' => [ 'description' => __( 'Field on which to sort.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'default' => 'date', ], 'direction' => [ 'description' => __( 'Direction on which to sort.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'default' => 'desc', ], ]; } /** * Item schema. * * @return array */ public function get_item_schema() { $schema = [ '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'transaction', 'type' => 'object', 'properties' => [ 'date' => [ 'description' => __( 'The date and time when the transaction was created.', 'woocommerce-payments' ), 'type' => 'string', 'format' => 'date-time', 'context' => [ 'view' ], ], 'transaction_id' => [ 'description' => __( 'A unique identifier for each transaction based on its transaction type.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'source_id' => [ 'description' => __( 'A unique source id for each transaction.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'channel' => [ 'description' => __( 'Indicates whether the transaction was made online or offline.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'payment_method' => [ 'description' => __( 'Specifies whether the payment method used was a card (Visa, Mastercard, etc.) or an Alternative Payment Method (APM) or Local Payment Method (LPM) (iDEAL, Apple Pay, Google Pay, etc.).', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'type' => [ 'description' => __( 'Specifies whether the payment method used was a card (Visa, Mastercard, etc.) or an Alternative Payment Method (APM) or Local Payment Method (LPM) (iDEAL, Apple Pay, Google Pay, etc.).', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'id' => [ 'description' => __( 'The payment method ID used to create the transaction type.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ], 'type' => [ 'description' => __( 'The type of the transaction.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'transaction_currency' => [ 'description' => __( 'The currency of the transaction.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'amount' => [ 'description' => __( 'The amount of the transaction.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'exchange_rate' => [ 'description' => __( 'The exchange rate of the transaction.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'deposit_currency' => [ 'description' => __( 'The currency of the store.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'fees' => [ 'description' => __( 'Transaction fees.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'customer' => [ 'description' => __( 'Customer details.', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'name' => [ 'name' => __( 'Customer name.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'email' => [ 'description' => __( 'Customer email.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'country' => [ 'description' => __( 'Customer country.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ], 'net_amount' => [ 'description' => __( 'Net amount.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'order_id' => [ 'description' => __( 'The identifier of the WooCommerce order associated with this transaction.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'risk_level' => [ 'description' => __( 'Fraud risk level.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'deposit_date' => [ 'description' => __( 'Deposit date of transaction', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'deposit_id' => [ 'description' => __( 'A unique identifier for the deposit.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'deposit_status' => [ 'description' => __( 'The status of the deposit', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ]; return $this->add_additional_fields_schema( $schema ); } } includes/reports/class-wc-rest-payments-reports-authorizations-controller.php 0000777 00000033402 15251156605 0024063 0 ustar 00 <?php /** * Class WC_REST_Payments_Reports_Authorizations_Controller * * @package WooCommerce\Payments\Reports */ use WCPay\Core\Server\Request\List_Authorizations; use WCPay\Core\Server\Request\Request_Utils; defined( 'ABSPATH' ) || exit; /** * REST controller for the authorization reports. */ class WC_REST_Payments_Reports_Authorizations_Controller extends WC_Payments_REST_Controller { /** * Endpoint path. * * @var string */ protected $rest_base = 'payments/reports/authorizations'; /** * Configure REST API routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_authorizations' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => $this->get_collection_params(), ], 'schema' => [ $this, 'get_item_schema' ], ] ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>\w+)', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_authorization' ], 'permission_callback' => [ $this, 'check_permission' ], ], 'schema' => [ $this, 'get_item_schema' ], ] ); } /** * Retrieve transactions to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_authorizations( $request ) { $wcpay_request = List_Authorizations::from_rest_request( $request ); $wcpay_request->set_page_size( $request->get_param( 'per_page' ) ?? 25 ); $user_timezone = $request->get_param( 'user_timezone' ); $filters = [ 'match' => $request->get_param( 'match' ), 'order_id_is' => $request->get_param( 'order_id' ), 'customer_email_is' => $request->get_param( 'customer_email' ), 'source_is' => $request->get_param( 'payment_method_type' ), ]; if ( $request->get_param( 'date_between' ) ) { $date_between_filter = $request->get_param( 'date_between' ); $filters['from_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $date_between_filter[0], $user_timezone ) ); $filters['to_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $date_between_filter[1], $user_timezone ) ); } if ( $request->get_param( 'date_before' ) ) { $filters['from_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $request->get_param( 'date_before' ), $user_timezone ) ); } if ( $request->get_param( 'date_after' ) ) { $filters['to_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $request->get_param( 'date_after' ), $user_timezone ) ); } $wcpay_request->set_filters( $filters ); $response = $wcpay_request->handle_rest_request( 'wcpay_list_authorizations_request' ); if ( is_wp_error( $response ) ) { return $response; } $data = []; foreach ( $response['data'] ?? [] as $authorization ) { $response = $this->prepare_item_for_response( $authorization, $request ); $data[] = $this->prepare_response_for_collection( $response ); } return rest_ensure_response( $data ); } /** * Retrieve transaction to respond with via API. * * @param WP_REST_Request $request Full data about the request. */ public function get_authorization( $request ) { $wcpay_request = List_Authorizations::create(); $wcpay_request->set_filters( [ 'charge_id_is' => $request->get_param( 'id' ) ] ); $wcpay_request->set_page_size( 1 ); // Set page size to limit to only one record. $response = $wcpay_request->handle_rest_request( 'wcpay_list_authorizations_request' ); if ( is_wp_error( $response ) ) { return $response; } $authorization = $response['data'][0] ?? null; if ( ! $authorization ) { return rest_ensure_response( [] ); } $response = $this->prepare_item_for_response( $authorization, $request ); $response = rest_ensure_response( $this->prepare_response_for_collection( $response ) ); return $response; } /** * Prepare each item for response. * * @param array|mixed $item Item to prepare. * @param WP_REST_Request $request Request instance. * * @return WP_REST_Response|WP_Error */ public function prepare_item_for_response( $item, $request ) { $prepared_item = []; $prepared_item['authorization_id'] = $item['charge_id']; $prepared_item['date'] = $item['created']; $prepared_item['payment_id'] = $item['payment_intent_id']; $prepared_item['channel'] = $item['channel']; $prepared_item['payment_method'] = [ 'type' => $item['source'], ]; $prepared_item['currency'] = $item['currency']; $prepared_item['amount'] = $item['amount']; $prepared_item['amount_captured'] = $item['amount_captured']; $prepared_item['fees'] = $item['fees']; $prepared_item['customer'] = [ 'name' => $item['customer_name'], 'email' => $item['customer_email'], 'country' => $item['customer_country'], ]; $prepared_item['net_amount'] = $item['net']; $prepared_item['order_id'] = $item['order_id']; $prepared_item['risk_level'] = $item['risk_level']; $context = $request['context'] ?? 'view'; $prepared_item = $this->add_additional_fields_to_object( $prepared_item, $request ); $prepared_item = $this->filter_response_by_context( $prepared_item, $context ); return rest_ensure_response( $prepared_item ); } /** * Collection args params. * * @return array[] */ public function get_collection_params() { return [ 'date_before' => [ 'description' => __( 'Filter transactions before this date.', 'woocommerce-payments' ), 'type' => 'string', 'format' => 'date-time', 'required' => false, ], 'date_after' => [ 'description' => __( 'Filter transactions after this date.', 'woocommerce-payments' ), 'type' => 'string', 'format' => 'date-time', 'required' => false, ], 'date_between' => [ 'description' => __( 'Filter transactions between these dates.', 'woocommerce-payments' ), 'type' => 'array', ], 'order_id' => [ 'description' => __( 'Filter transactions based on the associated order ID.', 'woocommerce-payments' ), 'type' => 'integer', 'required' => false, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ], 'deposit_id' => [ 'description' => __( 'Filter transactions based on the associated deposit ID.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'validate_callback' => 'rest_validate_request_arg', ], 'customer_email' => [ 'description' => __( 'Filter transactions based on the customer email.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'validate_callback' => 'rest_validate_request_arg', ], 'payment_method_type' => [ 'description' => __( 'Filter transactions based on the payment method used.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'validate_callback' => 'rest_validate_request_arg', ], 'type' => [ 'description' => __( 'Filter transactions where type is a specific value.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'validate_callback' => 'rest_validate_request_arg', ], 'match' => [ 'description' => __( 'Match filter for the transactions.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, ], 'user_timezone' => [ 'description' => __( 'Include timezone into date filtering.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, ], 'page' => [ 'description' => __( 'Page number.', 'woocommerce-payments' ), 'type' => 'integer', 'required' => false, 'default' => 1, 'minimum' => 1, ], 'per_page' => [ 'description' => __( 'Page size.', 'woocommerce-payments' ), 'type' => 'integer', 'required' => false, 'default' => 25, 'minimum' => 1, 'maximum' => 100, ], 'sort' => [ 'description' => __( 'Field on which to sort.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'default' => 'created', ], 'direction' => [ 'description' => __( 'Direction on which to sort.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, 'default' => 'desc', ], ]; } /** * Item schema. * * @return array */ public function get_item_schema() { $schema = [ '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'transaction', 'type' => 'object', 'properties' => [ 'date' => [ 'description' => __( 'The date and time when the transaction was created.', 'woocommerce-payments' ), 'type' => 'string', 'format' => 'date-time', 'context' => [ 'view' ], ], 'transaction_id' => [ 'description' => __( 'A unique identifier for each transaction based on its transaction type.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'source_id' => [ 'description' => __( 'A unique source id for each transaction.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'channel' => [ 'description' => __( 'Indicates whether the transaction was made online or offline.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'payment_method' => [ 'description' => __( 'Specifies whether the payment method used was a card (Visa, Mastercard, etc.) or an Alternative Payment Method (APM) or Local Payment Method (LPM) (iDEAL, Apple Pay, Google Pay, etc.).', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'type' => [ 'description' => __( 'Specifies whether the payment method used was a card (Visa, Mastercard, etc.) or an Alternative Payment Method (APM) or Local Payment Method (LPM) (iDEAL, Apple Pay, Google Pay, etc.).', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'id' => [ 'description' => __( 'The payment method ID used to create the transaction type.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ], 'type' => [ 'description' => __( 'The type of the transaction.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'transaction_currency' => [ 'description' => __( 'The currency of the transaction.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'amount' => [ 'description' => __( 'The amount of the transaction.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'exchange_rate' => [ 'description' => __( 'The exchange rate of the transaction.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'deposit_currency' => [ 'description' => __( 'The currency of the store.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'fees' => [ 'description' => __( 'Transaction fees.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'customer' => [ 'description' => __( 'Customer details.', 'woocommerce-payments' ), 'type' => 'object', 'context' => [ 'view' ], 'properties' => [ 'name' => [ 'name' => __( 'Customer name.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'email' => [ 'description' => __( 'Customer email.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'country' => [ 'description' => __( 'Customer country.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ], 'net_amount' => [ 'description' => __( 'Net amount.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'order_id' => [ 'description' => __( 'The identifier of the WooCommerce order associated with this transaction.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'risk_level' => [ 'description' => __( 'Fraud risk level.', 'woocommerce-payments' ), 'type' => 'number', 'context' => [ 'view' ], ], 'deposit_date' => [ 'description' => __( 'Deposit date of transaction', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'deposit_id' => [ 'description' => __( 'A unique identifier for the deposit.', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], 'deposit_status' => [ 'description' => __( 'The status of the deposit', 'woocommerce-payments' ), 'type' => 'string', 'context' => [ 'view' ], ], ], ]; return $this->add_additional_fields_schema( $schema ); } } includes/class-wc-payments-redirect-service.php 0000777 00000021155 15251156605 0015713 0 ustar 00 <?php /** * Class WC_Payments_Redirect_Service * * @package WooCommerce\Payments */ use WCPay\Core\Server\Request\Get_Account_Capital_Link; use WCPay\Core\Server\Request\Get_Account_Login_Data; use WCPay\Exceptions\API_Exception; use WCPay\Tracker; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Class handling redirects business logic. */ class WC_Payments_Redirect_Service { /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * Constructor for WC_Payments_Session_Service. * * @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client. */ public function __construct( WC_Payments_API_Client $payments_api_client ) { $this->payments_api_client = $payments_api_client; } /** * Calls wp_safe_redirect and exit. * * This method will end the execution immediately after the redirection. * * @param string $location The URL to redirect to. */ public function redirect_to( string $location ): void { wp_safe_redirect( $location ); exit; } /** * Redirects to a wcpay-connect URL which then handles the next step for the onboarding flow. * * This is a sure way to ensure that the user is redirected to the correct URL to continue their onboarding. * * @param string $from Source of the redirect. * @param array $additional_params Optional. Additional URL params to add to the redirect URL. */ public function redirect_to_wcpay_connect( string $from = '', array $additional_params = [] ): void { // Take the user to the 'wcpay-connect' URL. // We handle creating and redirecting to the account link there. $params = [ 'wcpay-connect' => '1', '_wpnonce' => wp_create_nonce( 'wcpay-connect' ), ]; $params = array_merge( $params, $additional_params ); if ( '' !== $from ) { $params['from'] = $from; } $connect_url = add_query_arg( $params, admin_url( 'admin.php' ) ); $this->redirect_to( $connect_url ); } /** * Redirects to the capital view offer page or overview page with error message. */ public function redirect_to_capital_view_offer_page(): void { $return_url = WC_Payments_Account::get_overview_page_url(); $refresh_url = add_query_arg( [ 'wcpay-loan-offer' => '' ], admin_url( 'admin.php' ) ); try { $request = Get_Account_Capital_Link::create(); $type = 'capital_financing_offer'; $request->set_type( $type ); $request->set_return_url( $return_url ); $request->set_refresh_url( $refresh_url ); $capital_link = $request->send(); Tracker::track_admin( 'wcpay_capital_view_offer_redirect' ); $this->redirect_to( $capital_link['url'] ); } catch ( Exception $e ) { $this->redirect_to_overview_page_with_error( [ 'wcpay-loan-offer-error' => '1' ] ); } } /** * Function to immediately redirect to the account link. * * @param array $args The arguments to be sent with the link request. */ public function redirect_to_account_link( array $args ): void { try { $link = $this->payments_api_client->get_link( $args ); if ( isset( $args['type'] ) && 'complete_kyc_link' === $args['type'] && isset( $link['state'] ) ) { set_transient( 'wcpay_stripe_onboarding_state', $link['state'], DAY_IN_SECONDS ); } $this->redirect_to( $link['url'] ); } catch ( API_Exception $e ) { $this->redirect_to_overview_page_with_error( [ 'wcpay-server-link-error' => '1' ] ); } } /** * Immediately redirect to the NOX page. * * Note that this function immediately ends the execution. * * @param string|null $from Optional. Location of the redirect. * @param string|null $source Optional. Where the merchant entered the onboarding flow. */ public function redirect_to_nox_flow( ?string $from = null, ?string $source = null ): void { // If the WC Admin version is less than 9.5, redirect to the Connect page instead. if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '9.9.3', '<' ) ) { $this->redirect_to_connect_page( null, $from, [ 'source' => $source ] ); } $params = [ 'page' => 'wc-settings', 'tab' => 'checkout', 'path' => '/woopayments/onboarding', ]; if ( ! empty( $from ) ) { $params['from'] = $from; } if ( ! empty( $source ) ) { $params['source'] = $source; } $this->redirect_to( admin_url( add_query_arg( $params, 'admin.php' ) ) ); } /** * Immediately redirect to the Connect page. * * Note that this function immediately ends the execution. * * @param string|null $error_message Optional. Error message to show in a notice. * @param string|null $from Optional. Source of the redirect. * @param array $additional_params Optional. Additional URL params to add to the redirect URL. */ public function redirect_to_connect_page( ?string $error_message = null, ?string $from = null, array $additional_params = [] ): void { $params = [ 'page' => 'wc-admin', 'path' => '/payments/connect', ]; if ( count( $params ) === count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended // We are already on the Connect page. Do nothing. return; } // If we were given an error message, store it in a very short-lived transient to show it on the page. if ( ! empty( $error_message ) ) { set_transient( WC_Payments_Account::ERROR_MESSAGE_TRANSIENT, $error_message, 30 ); } $params = array_merge( $params, $additional_params ); if ( ! empty( $from ) ) { $params['from'] = $from; } $this->redirect_to( admin_url( add_query_arg( $params, 'admin.php' ) ) ); } /** * Immediately redirect to the onboarding wizard. * * Note that this function immediately ends the execution. * * @param string|null $from Optional. Source of the redirect. * @param array $additional_params Optional. Additional URL params to add to the redirect URL. */ public function redirect_to_onboarding_wizard( ?string $from = null, array $additional_params = [] ): void { $params = [ 'page' => 'wc-admin', 'path' => '/payments/onboarding', ]; if ( count( $params ) === count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended // We are already in the onboarding wizard. Do nothing. return; } $params = array_merge( $params, $additional_params ); if ( ! empty( $from ) ) { $params['from'] = $from; } $this->redirect_to( admin_url( add_query_arg( $params, 'admin.php' ) ) ); } /** * Immediately redirect to the settings page. * * Note that this function immediately ends the execution. * * @param string|null $from Optional. Source of the redirect. * @param array $additional_params Optional. Additional URL params to add to the redirect URL. */ public function redirect_to_settings_page( ?string $from = null, array $additional_params = [] ): void { $params = [ 'page' => 'wc-settings', 'tab' => 'checkout', ]; if ( count( $params ) === count( array_intersect_assoc( $_GET, $params ) ) ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended // We are already in the settings page. Do nothing. return; } $params = array_merge( $params, $additional_params ); if ( ! empty( $from ) ) { $params['from'] = $from; } $this->redirect_to( admin_url( add_query_arg( $params, 'admin.php' ) ) ); } /** * Redirect to the overview page. * * @param string $from Optional. Source of the redirect. * @param array $additional_params Optional. Additional URL params to add to the redirect URL. * */ public function redirect_to_overview_page( string $from = '', array $additional_params = [] ): void { $params = $additional_params; if ( '' !== $from ) { $params['from'] = $from; } $this->redirect_to( add_query_arg( $params, WC_Payments_Account::get_overview_page_url() ) ); } /** * Redirect to the overview page with an error message. * * @param array $error The error data to show. */ public function redirect_to_overview_page_with_error( array $error ): void { $overview_url_with_error = add_query_arg( $error, WC_Payments_Account::get_overview_page_url() ); $this->redirect_to( $overview_url_with_error ); } /** * For the connected account, fetches the login url from the API and redirects to it. */ public function redirect_to_login(): void { $redirect_url = WC_Payments_Account::get_overview_page_url(); $request = Get_Account_Login_Data::create(); $request->set_redirect_url( $redirect_url ); $response = $request->send(); $login_data = $response->to_array(); $this->redirect_to( $login_data['url'] ); } } includes/core/class-mode.php 0000777 00000014147 15251156605 0012066 0 ustar 00 <?php /** * Class file for WCPay\Core\Mode. * * @package WooCommerce Payments */ namespace WCPay\Core; use WC_Payment_Gateway_WCPay; use Exception; /** * Controls the working mode of WooPayments. */ class Mode { /** * Holds the test mode flag. * * @var bool */ private $test_mode; /** * Holds the onboarding test mode flag. * * @var bool */ private $test_mode_onboarding; /** * Holds the dev mode flag. * * @var bool */ private $dev_mode; /** * Indicates the WCPay version which introduced the class. * * @var string */ const AVAILABLE_SINCE = '5.0.0'; /** * Environment types, which are used to automatically enter dev mode. * * @see wp_get_environment_type() * @see https://developer.wordpress.org/reference/functions/wp_get_environment_type/#description */ const DEV_MODE_ENVIRONMENTS = [ 'development', 'staging', ]; /** * Initializes the working mode of WooPayments. */ private function maybe_init() { // The object is only initialized once. if ( isset( $this->dev_mode ) && isset( $this->test_mode_onboarding ) && isset( $this->test_mode ) ) { return; } $dev_mode = ( // Plugin-specific dev mode. $this->is_wcpay_dev_mode_defined() // WordPress Dev Environment. || in_array( $this->get_wp_environment_type(), self::DEV_MODE_ENVIRONMENTS, true ) // WordPress Development mode. If any development mode is enabled, we'll fall back to dev as well. || '' !== $this->wp_get_development_mode() ); /** * Allows WooPayments to enter dev (aka sandbox) mode. * * @see https://woocommerce.com/document/woopayments/testing-and-troubleshooting/sandbox-mode/ * @param bool $dev_mode Whether to enter WooPayments in dev mode. */ $this->dev_mode = (bool) apply_filters( 'wcpay_dev_mode', $dev_mode ); // If dev mode is active, we enable test mode onboarding. $test_mode_onboarding = $this->dev_mode || \WC_Payments_Onboarding_Service::is_test_mode_enabled(); /** * Allows WooPayments to use test mode onboarding. * * @param bool $test_mode_onboarding Whether to use test mode onboarding. */ $this->test_mode_onboarding = (bool) apply_filters( 'wcpay_test_mode_onboarding', $test_mode_onboarding ); // If the current mode of onboarding is test, we will enable test mode payments processing. // Otherwise, follow the gateway settings. if ( $this->test_mode_onboarding ) { $test_mode = true; } else { // Getting the gateway settings directly from the database so the gateway doesn't need to be initialized. $settings_option_name = 'woocommerce_' . WC_Payment_Gateway_WCPay::GATEWAY_ID . '_settings'; $wcpay_settings = get_option( $settings_option_name ); $test_mode = 'yes' === ( $wcpay_settings['test_mode'] ?? false ); } /** * Allows WooPayments to process payments in test mode. * * @see https://woocommerce.com/document/woopayments/testing-and-troubleshooting/testing/#enabling-test-mode * @param bool $test_mode Whether to process payments in test mode. */ $this->test_mode = (bool) apply_filters( 'wcpay_test_mode', $test_mode ); } /** * Checks if live payment processing is enabled. * * @return bool * @throws Exception In case the class has not been initialized yet. */ public function is_live(): bool { $this->maybe_init(); return ! $this->test_mode; } /** * Checks if test payments processing is enabled. * * @return bool * @throws Exception In case the class has not been initialized yet. */ public function is_test(): bool { $this->maybe_init(); return $this->test_mode; } /** * Checks if test mode onboarding is enabled. * * @return bool * @throws Exception In case the class has not been initialized yet. */ public function is_test_mode_onboarding(): bool { $this->maybe_init(); return $this->test_mode_onboarding; } /** * Checks if dev mode is enabled. * * @return bool * @throws Exception In case the class has not been initialized yet. */ public function is_dev(): bool { $this->maybe_init(); return $this->dev_mode; } /** * Enable live payment processing. * * @return void */ public function live() { $this->test_mode = false; // We can't process live payments and be in test mode onboarding. $this->test_mode_onboarding = false; // We also can't be in dev mode. $this->dev_mode = false; } /** * Enable test payment processing. * * @return void */ public function test() { $this->test_mode = true; // Doesn't affect the onboarding mode or the dev mode. } /** * Enable test mode onboarding. * * @return void */ public function test_mode_onboarding() { $this->test_mode_onboarding = true; // When onboarding in test mode, we can only do test payment processing. $this->test_mode = true; } /** * Enable live mode onboarding. * * @return void */ public function live_mode_onboarding() { $this->test_mode_onboarding = false; // When onboarding in live mode, we can't be in dev mode. $this->dev_mode = false; // Doesn't affect the payments processing mode. } /** * Enable the gateway dev mode. * * Payments processing and onboarding are always in test mode when dev mode is active. * * @return void */ public function dev() { $this->dev_mode = true; // In dev mode, everything is in test mode. $this->test_mode = true; $this->test_mode_onboarding = true; } /** * Checks if the gateway is forced into dev mode through a constant. * * @return bool Whether `WCPAY_DEV_MODE` is defined and true. */ protected function is_wcpay_dev_mode_defined(): bool { return ( defined( 'WCPAY_DEV_MODE' ) && WCPAY_DEV_MODE ); } /** * Returns the current WP environment type. * * @return string|null */ protected function get_wp_environment_type() { return function_exists( 'wp_get_environment_type' ) ? wp_get_environment_type() : null; } /** * Returns the WordPress development mode setting. * * @return string */ protected function wp_get_development_mode(): string { return function_exists( 'wp_get_development_mode' ) ? wp_get_development_mode() : ''; // Support for older WordPress versions. } } includes/core/README.md 0000777 00000012333 15251156605 0010600 0 ustar 00 # WooCommerce Payments Core API This directory contains core WooCommerce Payments functionality, officially available for both internal use, and use by consumers and extensions. > 🔗 If you are contributing to WooCommerce Payments, please refer to [the core contributing guidelines](CONTRIBUTING.md). ## Definition The funtionality within this directory is meant to be expanded and maintained in a stable and reliable way, which will provide a solid base for building on top of and within WooCommerce Payments. Here is difference between the core and the rest of the plugin: ```mermaid flowchart TD subgraph Plugin CORE[WooCommerce Payments Core] WCPAY[Gateway] CONSUMER[Consumers] end EXTENSIONS[Extensions] SERVER[(WooCommerce Payments Server)] HOOKS[WordPress Actions and Filters] WCPAY <--> CORE CONSUMER <--> CORE EXTENSIONS <---> CORE CORE <--> SERVER HOOKS <--> CORE HOOKS <---> EXTENSIONS ``` 1. __WooCommerce Payments Core__ is a set of classes (and APIs), which (should) contain core WooCommerce Payments functionality like the connection to the server, main services (like [the customer service](service/customer-service.md)), and etc. 2. The rest of the __gateway__, meaning separate pieces of functionality like the REST API, admin views, etc. 3. __Consumers__ are pieces of code, which are tightly connected to WooCommerce Payments, but are not crutial to its core functionality. Examples include Multi-Currency, WooPay, and other extensions, some of which are built into WooCommerce Payments. 4. __Extensions__ are those use-cases, which are linked to WooCommerce Payments, but not build on top of/within the plugin. Those should use *WordPress actions and filters* to integrate with the gateway, and connect with the plugin through the classes, contained within the plugin core. ## Support and backwards compatibility The core is developed, and will be maintained with backwards compatibility in mind. ⚠️ This means that everything outside of this directory should be considered internal for the plugin, and may be changed without consideration for notice. To ensure stability and resilience, please avoid using non-core classes and methods within your extensions. The plugin is under active development, and if you need something, which is not present in this directory, please [open an issue](https://github.com/Automattic/woocommerce-payments/issues/new/). We will always consider the best way to expose the needed functionality. ## Available functionality WooCommerce Payments Core is under active development, and currently includes the APIs, listed below. ### Gateway Mode This is a singular `WCPay\Core\Mode` object, accessible through `WC_Payments::mode()`. It allows you to check whether the gateway is in any of the listed modes, as well as to force it into one of them: | Mode | Check | Setter | Description | |----------|------------|----------|---------------------------------------| | __Live__ | `is_live()`| `live()` | Standard working mode of the gateway. | | __Test__ | `is_test()`| `test()` | Allows test mode on live accounts. | | __Dev__ | `is_dev()` | `dev()` | Allows test accounts to be created. | During initialization, the following logic is used: 1. Sandbox mode would be entered if: - Either [WordPress's environment type](https://developer.wordpress.org/reference/functions/wp_get_environment_type/#description) is either `development` or `staging`. - or `WCPAY_DEV_MODE` is defined and set to boolean true. 2. Test mode is entered if: - Either Sandbox mode is already enabled. - or the gateway's test mode setting is on. 3. If the gateway is neither in sandbox or test mode, live mode is entered. To alter this behavior, you can use the `wcpay_dev_mode` and `wcpay_test_mode` filters, for example: ```php add_filter( 'wcpay_dev_mode', '__return_true' ); ``` This is done on the `init` action. > 🎗️ The object is not accessible before the `init` WordPress action. Once the mode has been initialized, the gateway will work in that mode. The setters (from the table above) may be used to switch the working mode during runtime, but it should be done with caution, as other functionality may already be initialized in the main mode, hence it is recommended to switch back once done. __Checking the current mode:__ ```php // Check for live mode payments processing: WC_Payments::mode()->is_live(); // Check for test mode payments processing: WC_Payments::mode()->is_test(); // Check for test mode onboarding: WC_Payments::mode()->is_test_mode_onboarding(); // Check for overall gateway dev mode: WC_Payments::mode()->is_dev(); ``` ### Services WooCommerce Payments contains a comprehensive set of internal services, but as explained at the beginning of this document, they are subject to change without notice. Instead, please use the following services available in core: - [🔗 `WC_Payments_Customer_Service_API`](service/customer-service.md) ### Server requests & responses Communication with the WooCommerce Payments Server is currently done through a mix of the `WC_Payments_API_Client`, and request classes, and being migrated to fully use the latter. 🔗 [Read the full docs for server communication here](server/README.md). includes/core/server/request/class-woopay-create-and-confirm-setup-intention.md 0000777 00000004246 15251156605 0024065 0 ustar 00 # `WooPay_Create_and_Confirm_Setup_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\WooPay_Create_and_Confirm_Setup_Intention` class is used to construct the request for creating and confirming a setup intention for WooPay. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-----------------------------------|-------------------------------------------------------|:---------:|:--------:|:-------------:| | `customer` | `set_customer( string $customer_id )` | Yes | Yes | - | | `is_platform_payment_method` | `set_is_platform_payment_method( $is = true )` | - | - | - | | `description` | `set_metadata( array $metadata )` | - | - | - | | `payment_method` | `set_payment_method( string $payment_method_id )` | - | - | - | | `save_in_platform_account` | `set_save_in_platform_account( $save = true )` | - | - | - | | `save_payment_method_to_platform` | `set_save_payment_method_to_platform( $save = true )` | - | - | - | ## Filter - Name: `wcpay_create_and_confirm_setup_intent_request` - Arguments: - `WCPay\Payment_Information $payment_information` - `bool $save_in_platform_account` - `bool $save_payment_method_to_platform` ## Example: ```php $request = WooPay_Create_and_Confirm_Setup_Intention::create(); $request->set_customer( $customer_id ); $request->set_is_platform_payment_method( $is ); $request->set_metadata( $metadata ); $request->set_payment_method( $payment_method_id ); $request->set_save_in_platform_account( $save ); $request->set_save_payment_method_to_platform( $save ); $request->assign_hook( 'wcpay_create_and_confirm_setup_intent_request' ); $request->set_hook_args( $payment_information, $save_in_platform_account, $save_payment_method_to_platform ); $request->send(); ``` includes/core/server/request/class-get-intention.php 0000777 00000003565 15251156605 0016726 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Create_Intent. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments; use WC_Payments_Http_Interface; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for getting intents. */ class Get_Intention extends Request { /** * Intent id. * * @var string $intent_id */ private $intent_id; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_get_intent_request'; /** * Class constructor. * * @param WC_Payments_API_Client $api_client Api client. * @param WC_Payments_Http_Interface $http_interface Http interface. * @param string $intent_id Intent id. * * @throws Invalid_Request_Parameter_Exception */ public function __construct( WC_Payments_API_Client $api_client, WC_Payments_Http_Interface $http_interface, string $intent_id ) { $this->validate_stripe_id( $intent_id ); parent::__construct( $api_client, $http_interface ); $this->intent_id = $intent_id; } /** * Returns the request's API. * * @return string * @throws Invalid_Request_Parameter_Exception */ public function get_api(): string { return WC_Payments_API_Client::INTENTIONS_API . '/' . $this->intent_id; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'GET'; } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { return $this->api_client->deserialize_payment_intention_object_from_array( $response ); } } includes/core/server/request/class-activate-pm-promotion.php 0000777 00000002426 15251156605 0020373 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Activate_PM_Promotion. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Server\Request; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WC_Payments_API_Client; /** * Request class for activating a PM promotion. */ class Activate_PM_Promotion extends Request { /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_activate_pm_promotion_request'; /** * Returns the request's API. * * @return string * * @throws \InvalidArgumentException If ID is not set. */ public function get_api(): string { if ( empty( $this->id ) ) { throw new \InvalidArgumentException( 'Promotion ID is required for activation' ); } return WC_Payments_API_Client::PROMOTIONS_API . '/' . $this->id . '/activate'; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * Sets the promotion instance ID, which will be used in the request URL. * * @param string $id Sets the promotion instance ID, which will be used in the request URL. */ protected function set_id( string $id ) { $this->id = $id; } } includes/core/server/request/class-list-transactions.md 0000777 00000010651 15251156605 0017426 0 ustar 00 # `List_Transactions` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\List_Transactions` class is used to construct the request for listing transactions. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |----------------------------|-----------------------------------------------------------------|:---------:|:--------:|:-------------:| | `customer_currency_is` | `set_customer_currency_is( string $customer_currency_is )` | - | - | - | | `customer_currency_is_not` | `set_customer_currency_is_not( string $currency )` | - | - | - | | `deposit_id` | `set_deposit_id( $deposit_id )` | - | - | - | | `loan_id_is` | `set_loan_id_is( string $loan_id )` | - | - | - | | `match` | `set_match( string $match )` | - | - | - | | `page` | `set_page( int $page )` | Yes | - | - | | `pagesize` | `set_page_size( int $page_size )` | Yes | - | `25` | | `search` | `set_search( array $search )` | - | - | - | | `sort` | `set_sort_by( string $sort )` | Yes | - | `'created'` | | `direction` | `set_sort_direction( string $direction )` | Yes | - | `'desc'` | | `store_currency_is` | `set_store_currency_is( string $currency )` | - | - | - | | `type_is` | `set_type_is( string $type_is )` | - | - | - | | `type_is_not` | `set_type_is_not( string $type_is_not )` | - | - | - | | `source_device_is` | `set_source_device_is( string $source_device_is )` | - | - | - | | `source_device_is_not` | `set_source_device_is_not( string $source_device_is_not )` | - | - | - | | `channel_is` | `set_channel_is( string $channel_is )` | - | - | - | | `channel_is_not` | `set_channel_is_not( string $channel_is_not )` | - | - | - | | `customer_country_is` | `set_customer_country_is( string $customer_country_is )` | - | - | - | | `customer_country_is_not` | `set_customer_country_is_not( string $customer_country_is_not )`| - | - | - | | `risk_level_is` | `set_risk_level_is( string $risk_level_is )` | - | - | - | | `risk_level_is_not` | `set_risk_level_is_not( string $risk_level_is_not )` | - | - | - | ## Filter - Name: `wcpay_list_transactions_request` - Arguments: `$request` ## Example: ```php $request = List_Transactions::create(); $request->set_customer_currency_is( $customer_currency_is ); $request->set_customer_currency_is_not( $currency ); $request->set_deposit_id( $deposit_id ); $request->set_loan_id_is( $loan_id ); $request->set_match( $match ); $request->set_page( $page ); $request->set_page_size( $page_size ); $request->set_search( $search ); $request->set_sort_by( $sort ); $request->set_sort_direction( $direction ); $request->set_store_currency_is( $currency ); $request->set_type_is( $type_is ); $request->set_type_is_not( $type_is_not ); $request->set_source_device_is( $source_device_is ); $request->set_source_device_is_not( $source_device_is_not ); $request->set_channel_is( $channel_is ); $request->set_channel_is_not( $channel_is_not ); $request->set_customer_country_is( $customer_country_is ); $request->set_customer_country_not( $customer_country_is_not ); $request->set_risk_level_is( $risk_level_is ); $request->set_risk_level_is_not( $risk_level_is_not ); $request->send(); ``` includes/core/server/request/class-get-account-login-data.php 0000777 00000002736 15251156605 0020367 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Get_Account_Login_Data. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for getting one-time dashboard login url. */ class Get_Account_Login_Data extends Request { use Use_Test_Mode_Only_When_Test_Mode_Onboarding; const REQUIRED_PARAMS = [ 'redirect_url', ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wpcay_get_account_login_data'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::ACCOUNTS_API . '/login_links'; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * If true, the request will be signed with the user token rather than blog token. * * @return bool */ public function should_use_user_token(): bool { return true; } /** * Sets the redirect URL. * * @param string $redirect_url URL to navigate back to from the dashboard. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_redirect_url( string $redirect_url ) { $this->validate_redirect_url( $redirect_url ); $this->set_param( 'redirect_url', $redirect_url ); } } includes/core/server/request/class-get-request.md 0000777 00000002454 15251156605 0016214 0 ustar 00 # `Get_Request` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Get_Request` class is used to construct the basic and simple request that uses HTTP GET method. ## Parameters When creating `Get_Request` requests, the ID parameter could be passed to the `::create()` method. It's optional argument and used when you want to pass some resource ID to the request. | Parameter | Setter | Immutable | Required | Default value | |-----------|--------------------------|:---------:|:--------:|:-------------:| | `id` | `set_id( $id )` | Yes | No | - | | `api` | `set_api( string $api )` | - | Yes | - | ## Examples: ```php $request = Request::get( ('deposits', $id ); $request->assign_hook( 'wcpay_get_deposit_request' ); $request->send(); ``` ```php $request = Request::get( ('deposits' ); // Without ID argument. $request->assign_hook( 'wcpay_get_deposits_request' ); $request->send(); ``` You could also create request with the following code, but it is not recommended. ```php $request = Get_Request::create( $id ); $request->set_method ('deposits') $request->assign_hook( 'wcpay_get_deposit_request' ); $request->send(); ``` includes/core/server/request/class-cancel-intention.php 0000777 00000003242 15251156605 0017364 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Update_Intent. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for canceling intents. */ class Cancel_Intention extends Request { use Intention; use Level3; const IMMUTABLE_PARAMS = []; const REQUIRED_PARAMS = []; const DEFAULT_PARAMS = []; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_cancel_intent_request'; /** * Sets the intent ID, which will be used in the request URL. * * @param string $id Sets the intent ID, which will be used in the request URL. * * @throws Invalid_Request_Parameter_Exception */ protected function set_id( string $id ) { $this->validate_stripe_id( $id ); $this->id = $id; } /** * Returns the request's API. * * @return string * @throws Invalid_Request_Parameter_Exception */ public function get_api(): string { return WC_Payments_API_Client::INTENTIONS_API . '/' . $this->id . '/cancel'; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { return $this->api_client->deserialize_payment_intention_object_from_array( $response ); } } includes/core/server/request/class-refund-charge.md 0000777 00000001573 15251156605 0016462 0 ustar 00 # `Refund_Charge` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Refund_Charge` class is used to construct the request for refunding a charge. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-----------|-----------------------------------|:---------:|:--------:|:-------------:| | `charge` | `set_charge( string $charge_id )` | Yes | Yes | None | | `amount` | `set_amount( int $amount )` | No | No | null | ## Filter - Name: `wcpay_refund_charge_request` - Arguments: None ## Example: ```php $request = Refund_Charge::create( ); $request->set_charge( 'ch_xxxxx' ); $request->set_amount( 100 ); // It is not required. You can also skip this setter. $request->send(); ``` includes/core/server/request/class-list-transactions.php 0000777 00000021015 15251156605 0017611 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\List_Transactions. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments_API_Client; use WC_Payments_DB; use WC_Payments_Utils; use WCPay\Core\Server\Response; use WCPay\Core\Server\Request\Request_Utils; use WP_REST_Request; /** * Request class for listing transactions. */ class List_Transactions extends Paginated { use Date_Parameters; use Order_Info; const DEFAULT_PARAMS = [ 'sort' => 'date', 'direction' => 'desc', ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_list_transactions_request'; /** * Set deposit id. * * @param mixed $deposit_id Deposit id. * * @return void */ public function set_deposit_id( $deposit_id ) { $this->set_param( 'deposit_id', $deposit_id ); } /** * Get api URI. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::TRANSACTIONS_API; } /** * Used to prepare request from WP Rest data. * * @param WP_REST_Request $request Request object. * * @return static */ public static function from_rest_request( $request ) { $wcpay_request = parent::from_rest_request( $request ); $date_between_filter = $request->get_param( 'date_between' ); $user_timezone = $request->get_param( 'user_timezone' ); if ( ! is_null( $date_between_filter ) ) { $date_between_filter = array_map( function ( $transaction_date ) use ( $user_timezone ) { return Request_Utils::format_transaction_date_by_timezone( $transaction_date, $user_timezone ); }, $date_between_filter ); } $filters = [ 'match' => $request->get_param( 'match' ), 'date_before' => Request_Utils::format_transaction_date_by_timezone( $request->get_param( 'date_before' ), $user_timezone ), 'date_after' => Request_Utils::format_transaction_date_by_timezone( $request->get_param( 'date_after' ), $user_timezone ), 'date_between' => $date_between_filter, 'type_is' => $request->get_param( 'type_is' ), 'type_is_not' => $request->get_param( 'type_is_not' ), 'type_is_in' => (array) $request->get_param( 'type_is_in' ), 'source_device_is' => $request->get_param( 'source_device_is' ), 'source_device_is_not' => $request->get_param( 'source_device_is_not' ), 'channel_is' => $request->get_param( 'channel_is' ), 'channel_is_not' => $request->get_param( 'channel_is_not' ), 'customer_country_is' => $request->get_param( 'customer_country_is' ), 'customer_country_is_not' => $request->get_param( 'customer_country_is_not' ), 'risk_level_is' => $request->get_param( 'risk_level_is' ), 'risk_level_is_not' => $request->get_param( 'risk_level_is_not' ), 'store_currency_is' => $request->get_param( 'store_currency_is' ), 'customer_currency_is' => $request->get_param( 'customer_currency_is' ), 'customer_currency_is_not' => $request->get_param( 'customer_currency_is_not' ), 'source_is' => $request->get_param( 'source_is' ), 'source_is_not' => $request->get_param( 'source_is_not' ), 'loan_id_is' => $request->get_param( 'loan_id_is' ), 'search' => (array) $request->get_param( 'search' ), 'deposit_id' => $request->get_param( 'deposit_id' ), ]; $wcpay_request->set_filters( $filters ); return $wcpay_request; } /** * Set match. * * @param string $match Match. * * @return void */ public function set_match( string $match ) { $this->set_param( 'match', $match ); } /** * Set loan id is. * * @param string $loan_id Loan id. * * @return void */ public function set_loan_id_is( string $loan_id ) { $this->set_param( 'loan_id_is', $loan_id ); } /** * Set currency is. * * @param string $currency Store currency. * * @return void */ public function set_store_currency_is( string $currency ) { $this->set_param( 'store_currency_is', $currency ); } /** * Set customer currency is. * * @param string $customer_currency_is Store currency. * * @return void */ public function set_customer_currency_is( string $customer_currency_is ) { $this->set_param( 'customer_currency_is', $customer_currency_is ); } /** * Set customer currency is not. * * @param string $currency Store currency. * * @return void */ public function set_customer_currency_is_not( string $currency ) { $this->set_param( 'customer_currency_is_not', $currency ); } /** * Set search. * * @param array $search Search term. * * @return void */ public function set_search( array $search ) { if ( ! empty( $search ) ) { $search = WC_Payments_Utils::map_search_orders_to_charge_ids( $search ); $this->set_param( 'search', $search ); } } /** * Set type is. * * @param string $type_is Type is. * * @return void */ public function set_type_is( string $type_is ) { $this->set_param( 'type_is', $type_is ); } /** * Set type is not. * * @param string $type_is_not Type is not. * * @return void */ public function set_type_is_not( string $type_is_not ) { $this->set_param( 'type_is_not', $type_is_not ); } /** * Set Source device type is. * * @param string $source_device_is Source device type is. * * @return void */ public function set_source_device_is( string $source_device_is ) { $this->set_param( 'source_device_is', $source_device_is ); } /** * Set Source Device type is not. * * @param string $source_device_is_not Source Device type is not. * * @return void */ public function set_source_device_is_not( string $source_device_is_not ) { $this->set_param( 'source_device_is_not', $source_device_is_not ); } /** * Set Channel type is. * * @param string $channel_is Channel type is. * * @return void */ public function set_channel_is( string $channel_is ) { $this->set_param( 'channel_is', $channel_is ); } /** * Set Channel type is not. * * @param string $channel_is_not Channel type is not. * * @return void */ public function set_channel_is_not( string $channel_is_not ) { $this->set_param( 'channel_is_not', $channel_is_not ); } /** * Set Customer country is. * * @param string $customer_country_is Customer country is. * * @return void */ public function set_customer_country_is( string $customer_country_is ) { $this->set_param( 'customer_country_is', $customer_country_is ); } /** * Set Customer country is not. * * @param string $customer_country_is_not Customer country is not. * * @return void */ public function set_customer_country_is_not( string $customer_country_is_not ) { $this->set_param( 'customer_country_is_not', $customer_country_is_not ); } /** * Set Risk level is. * * @param string $risk_level_is Risk level is. * * @return void */ public function set_risk_level_is( string $risk_level_is ) { $this->set_param( 'risk_level_is', $risk_level_is ); } /** * Set Risk level is not. * * @param string $risk_level_is_not Risk level is not. * * @return void */ public function set_risk_level_is_not( string $risk_level_is_not ) { $this->set_param( 'risk_level_is_not', $risk_level_is_not ); } /** * Return formatted response. * * @param mixed $response Transactions from server. * * @return Response */ public function format_response( $response ) { $wcpay_db = new WC_Payments_DB(); $charge_ids = array_column( $response['data'], 'charge_id' ); $orders_with_charge_ids = count( $charge_ids ) ? $wcpay_db->orders_with_charge_id_from_charge_ids( $charge_ids ) : []; // Add order information to each transaction available. // TODO: Throw exception when `$response` or `$transaction` don't have the fields expected? if ( isset( $response['data'] ) ) { foreach ( $response['data'] as &$transaction ) { foreach ( $orders_with_charge_ids as $order_with_charge_id ) { if ( $order_with_charge_id['charge_id'] === $transaction['charge_id'] && ! empty( $transaction['charge_id'] ) ) { $order = $order_with_charge_id['order']; $transaction['order'] = $this->build_order_info( $order ); $transaction['payment_intent_id'] = $order->get_meta( '_intent_id' ); } } } // Securing future changes from modifying reference content. unset( $transaction ); } return new Response( $response ); } } includes/core/server/request/class-woopay-create-and-confirm-setup-intention.php 0000777 00000002360 15251156605 0024247 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\WooPay_Create_And_Confirm_Setup_Intention. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; /** * Request class for creating woopay setup intents. */ class WooPay_Create_And_Confirm_Setup_Intention extends Create_And_Confirm_Setup_Intention { const DEFAULT_PARAMS = [ 'save_in_platform_account' => false, 'is_platform_payment_method' => false, 'save_payment_method_to_platform' => false, ]; /** * Toggles the flag, which indicates that this is a platform payment method. * * @param bool $is Whether it is indeed a platform payment method (Optional). */ public function set_is_platform_payment_method( $is = true ) { $this->set_param( 'is_platform_payment_method', $is ); } /** * Save to platform account. * * @param bool $save save to platform account or not. */ public function set_save_in_platform_account( $save = true ) { $this->set_param( 'save_in_platform_account', $save ); } /** * Save payment method to platform. * * @param bool $save save payment method to platform. */ public function set_save_payment_method_to_platform( $save = true ) { $this->set_param( 'save_payment_method_to_platform', $save ); } } includes/core/server/request/class-get-charge.md 0000777 00000001257 15251156605 0015755 0 ustar 00 # `Get_Charge` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Get_Charge` class is used to construct the request for retrieving a charge. ## Parameters When creating `Get_Charge` requests, the item ID must be provided to the `::create()` method. The identifier should be in the `ch_XXX` format. There are no additional parameters for this request. ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_get_charge_request` - Arguments: None or charge id. ## Example: ```php $request = Get_Charge::create( $id ); $request->send(); ``` includes/core/server/request/class-create-setup-intention.md 0000777 00000002014 15251156605 0020345 0 ustar 00 # `Create_Setup_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Create_Setup_Intention` class is used to construct the request for creating a setup intention. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |------------------------|------------------------------------------------------|:---------:|:--------:|:-------------:| | `customer` | `set_customer( string $customer_id )` | Yes | Yes | - | | `payment_method_types` | `set_payment_method_types( array $payment_methods )` | - | Yes | - | ## Filter - Name: `wcpay_create_setup_intent_request` - Arguments: None ## Example: ```php $request = Create_Setup_Intention::create(); $request->set_customer( $customer_id ); $request->set_payment_method_types( $payment_methods ); $request->send(); ``` includes/core/server/request/class-get-account-capital-link.php 0000777 00000004164 15251156605 0020715 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Get_Account_Capital_Link. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for getting a one-time capital link. * Expected response is an account link object with create, expires_at, and url fields. */ class Get_Account_Capital_Link extends Request { const REQUIRED_PARAMS = [ 'type', 'return_url', 'refresh_url', ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_get_account_capital_link'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::ACCOUNTS_API . '/capital_links'; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * If true, the request will be signed with the user token rather than blog token. * * @return bool */ public function should_use_user_token(): bool { return true; } /** * Sets the type of capital link. * * @param string $type The type of link to be requested. * * @return void */ public function set_type( string $type ) { $this->set_param( 'type', $type ); } /** * Sets the redirect URL. * * @param string $return_url URL to navigate back to from the dashboard. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_return_url( string $return_url ) { $this->validate_redirect_url( $return_url ); $this->set_param( 'return_url', $return_url ); } /** * Sets the refresh URL. * * @param string $refresh_url URL to navigate to if the link expired, has been previously-visited, or is otherwise invalid. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_refresh_url( string $refresh_url ) { $this->validate_redirect_url( $refresh_url ); $this->set_param( 'refresh_url', $refresh_url ); } } includes/core/server/request/class-create-and-confirm-setup-intention.php 0000777 00000010013 15251156605 0022725 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Create_Setup_Intention. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for creating setup intents. */ class Create_And_Confirm_Setup_Intention extends Request { use Intention; use Level3; const IMMUTABLE_PARAMS = [ 'customer', 'confirm' ]; const REQUIRED_PARAMS = [ 'customer' ]; const DEFAULT_PARAMS = [ 'confirm' => 'true', 'metadata' => [], ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_create_and_confirm_setup_intention_request'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::SETUP_INTENTS_API; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * Customer setter. * * @param string $customer_id ID of the customer making the payment. * @return void * * @throws Invalid_Request_Parameter_Exception */ public function set_customer( string $customer_id ) { $this->validate_stripe_id( $customer_id, 'cus' ); $this->set_param( 'customer', $customer_id ); } /** * Set metadata. * * @param array $metadata Metadata to set. * * @return void */ public function set_metadata( array $metadata ) { if ( isset( $metadata['order_number'] ) ) { $description = $this->get_intent_description( $metadata['order_number'] ); $this->set_param( 'description', $description ); } $this->set_param( 'metadata', $metadata ); } /** * Payment method setter. * * @param string $payment_method_id ID of payment method to process charge with. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_payment_method( string $payment_method_id ) { // Including the 'card' prefix to support subscription renewals using legacy payment method IDs. $this->validate_stripe_id( $payment_method_id, [ 'pm', 'src', 'card' ] ); $this->set_param( 'payment_method', $payment_method_id ); } /** * Confirmation token setter. * * Used by Express Checkout Element (ECE) which provides a confirmation token * instead of a payment method ID. * * @param string $confirmation_token The confirmation token. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_confirmation_token( string $confirmation_token ) { $this->validate_stripe_id( $confirmation_token, 'ctoken' ); $this->set_param( 'confirmation_token', $confirmation_token ); } /** * Payment methods setter. * * @param array $payment_methods An array of payment methods that might be used for the payment. * @throws Invalid_Request_Parameter_Exception When there are no payment methods provided. */ public function set_payment_method_types( array $payment_methods ) { // Hard to validate without hardcoding a list here. if ( empty( $payment_methods ) ) { throw new Invalid_Request_Parameter_Exception( esc_html__( 'Intentions require at least one payment method', 'woocommerce-payments' ), 'wcpay_core_invalid_request_parameter_missing_payment_method_types' ); } $this->set_param( 'payment_method_types', $payment_methods ); } /** * Mandate data setter. * * @param array $mandate_data Array containing details about mandate to create. * * @return void */ public function set_mandate_data( array $mandate_data ) { $this->set_param( 'mandate_data', $mandate_data ); } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { return $this->api_client->deserialize_setup_intention_object_from_array( $response ); } } includes/core/server/request/class-create-and-confirm-intention.php 0000777 00000006066 15251156605 0021604 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Create_Intention. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WC_Payments_API_Client; /** * Request class for creating intents. */ class Create_And_Confirm_Intention extends Create_Intention { const IMMUTABLE_PARAMS = [ // Those are up to us, we have to decide. 'amount', 'currency', 'payment_method', 'confirmation_token', 'payment_method_update_data', 'return_url', ]; const REQUIRED_PARAMS = [ 'amount', 'currency', 'customer', 'metadata', ]; const DEFAULT_PARAMS = [ 'confirm' => true, // By the definition of the request. 'capture_method' => 'automatic', ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_create_and_confirm_intent_request'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::INTENTIONS_API; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * If the payment method should be saved to the store, this enables future usage. */ public function setup_future_usage() { $this->set_param( 'setup_future_usage', 'off_session' ); } /** * Off-session setter. * * @param bool $off_session Whether the payment is off-session (merchant-initiated), or on-session (customer-initiated). */ public function set_off_session( bool $off_session = true ) { // This one is tricky. We can have `true`, but otherwise we need to get rid of the parameter. if ( $off_session ) { $this->set_param( 'off_session', true ); } else { $this->unset_param( 'off_session' ); } } /** * Payment methods setter. * * @param array $payment_methods An array of payment methods that might be used for the payment. * @throws Invalid_Request_Parameter_Exception When there are no payment methods provided. */ public function set_payment_methods( array $payment_methods ) { $this->set_param( 'payment_method_types', $payment_methods ); } /** * Payment method update data setter. * * @param array $payment_method_update_data Data to update on payment method. * * @return void */ public function set_payment_method_update_data( array $payment_method_update_data ) { $this->set_param( 'payment_method_update_data', $payment_method_update_data ); } /** * CVC confirmation setter. * * @param string $cvc_confirmation The CVC confirmation for this payment method (Optional). */ public function set_cvc_confirmation( $cvc_confirmation = null ) { $this->set_param( 'cvc_confirmation', $cvc_confirmation ); } /** * Return URL setter. * * @param string $return_url The URL to redirect the customer back to after they authenticate their payment on the payment method's site. */ public function set_return_url( $return_url ) { $this->set_param( 'return_url', $return_url ); } } includes/core/server/request/class-list-deposits.md 0000777 00000003701 15251156605 0016546 0 ustar 00 # `List_Deposits` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\List_Deposits` class is used to construct the request for listing deposits. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |---------------------|------------------------------------------------------|:---------:|:--------:|:-------------:| | `match` | `set_match( string $match )` | - | - | - | | `page` | `set_page( int $page )` | Yes | - | - | | `pagesize` | `set_page_size( int $page_size )` | Yes | - | `25` | | `sort` | `set_sort_by( string $sort )` | Yes | - | `'created'` | | `direction` | `set_sort_direction( string $direction )` | Yes | - | `'desc'` | | `status_is` | `set_status_is( string $status_is )` | - | - | - | | `status_is_not` | `set_status_is_not( string $status_is_not )` | - | - | - | | `store_currency_is` | `set_store_currency_is( string $store_currency_is )` | - | - | - | ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_list_deposits_request` - Arguments: `$request` ## Example: ```php $request = List_Deposits::create(); $request->set_match( $match ); $request->set_page( $page ); $request->set_page_size( $page_size ); $request->set_sort_by( $sort ); $request->set_sort_direction( $direction ); $request->set_status_is( $status_is ); $request->set_status_is_not( $status_is_not ); $request->set_store_currency_is( $store_currency_is ); $request->send(); ``` includes/core/server/request/class-update-account.md 0000777 00000007476 15251156605 0016674 0 ustar 00 # `Update_Account` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Update_Account` class is used to construct the request for creating an intention. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-----------------------------------|----------------------------------------------------------------------------------|:---------:|:--------:|:-------------:| | `statement_descriptor` | `set_statement_descriptor( string $statement_descriptor )` | - | - | - | | `business_name` | `set_business_name( string $business_name )` | - | - | - | | `business_url` | `set_business_url( string $business_url )` | - | - | - | | `business_support_address` | `set_business_support_address( string $business_support_address )` | - | - | - | | `business_support_email` | `set_business_support_email( string $business_support_email )` | - | - | - | | `business_support_phone` | `set_business_support_phone( string $business_support_phone )` | - | - | - | | `branding_logo` | `set_branding_logo( string $branding_logo )` | - | - | - | | `branding_icon` | `set_branding_icon( string $branding_icon )` | - | - | - | | `branding_primary_color` | `set_branding_primary_color( string $branding_primary_color )` | - | - | - | | `branding_secondary_color` | `set_branding_secondary_color( string $branding_secondary_color )` | - | - | - | | `deposit_schedule_interval` | `set_deposit_schedule_interval( string $deposit_schedule_interval )` | - | - | - | | `deposit_schedule_weekly_anchor` | `set_deposit_schedule_weekly_anchor( string $deposit_schedule_weekly_anchor )` | - | - | - | | `deposit_schedule_monthly_anchor` | `set_deposit_schedule_monthly_anchor( string $deposit_schedule_monthly_anchor )` | - | - | - | | `locale` | `set_locale( string $locale )` | - | - | - | ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_update_account_settings` - Arguments: None. ## Example: ```php $request = Update_Account::create(); $request->set_statement_descriptor( $statement_descriptor ); $request->set_business_name( $business_name ); $request->set_business_url( $business_url ); $request->set_business_support_address( $business_support_address ); $request->set_business_support_email( $business_support_email ); $request->set_business_support_phone( $business_support_phone ); $request->set_branding_logo( $branding_logo ); $request->set_branding_icon( $branding_icon ); $request->set_branding_primary_color( $branding_primary_color ); $request->set_branding_secondary_color( $branding_secondary_color ); $request->set_deposit_schedule_interval( $deposit_schedule_interval ); $request->set_deposit_schedule_weekly_anchor( $deposit_schedule_weekly_anchor ); $request->set_deposit_schedule_monthly_anchor( $deposit_schedule_monthly_anchor ); $request->set_locale( $locale ); $request->send(); ``` includes/core/server/request/class-create-intention.php 0000777 00000013051 15251156605 0017401 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Create_Intention. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for creating intents. */ class Create_Intention extends Request { use Intention; use Level3; const IMMUTABLE_PARAMS = [ 'amount' ]; const REQUIRED_PARAMS = [ 'amount', 'currency' ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_create_intent_request'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::INTENTIONS_API; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * Payment method setter. * * @param string $payment_method_id ID of payment method to process charge with. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_payment_method( string $payment_method_id ) { // Including the 'card' prefix to support subscription renewals using legacy payment method IDs. $this->validate_stripe_id( $payment_method_id, [ 'pm', 'src', 'card' ] ); $this->set_param( 'payment_method', $payment_method_id ); } /** * Confirmation token setter. * * @param string $confirmation_token The confirmation token. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_confirmation_token( string $confirmation_token ) { $this->validate_stripe_id( $confirmation_token, 'ctoken' ); $this->set_param( 'confirmation_token', $confirmation_token ); } /** * Payment methods type setter. * * @param array $payment_methods List of payment methods. * * @return void */ public function set_payment_method_types( array $payment_methods ) { $this->set_param( 'payment_method_types', $payment_methods ); } /** * Customer setter. * * @param string $customer_id ID of the customer making the payment. * @return void * * @throws Invalid_Request_Parameter_Exception */ public function set_customer( string $customer_id ) { $this->validate_stripe_id( $customer_id, 'cus' ); $this->set_param( 'customer', $customer_id ); } /** * Stores the amount for the intent. * * @param int $amount The amount in ToDo units. * @throws Invalid_Request_Parameter_Exception */ public function set_amount( int $amount ) { $this->validate_is_larger_than( $amount, 0 ); $this->set_param( 'amount', $amount ); } /** * Currency code setter. * * @param string $currency_code Currency to charge in. * @throws Invalid_Request_Parameter_Exception When the currency code is invalid. */ public function set_currency_code( string $currency_code ) { $this->validate_currency_code( $currency_code ); $this->set_param( 'currency', $currency_code ); } /** * Capture method setter. * * @param bool $manual_capture Whether to capture funds via manual action. */ public function set_capture_method( bool $manual_capture = false ) { $this->set_param( 'capture_method', $manual_capture ? 'manual' : 'automatic' ); } /** * Metadata setter. * * @param array $metadata Meta data values to be sent along with payment intent creation. * @throws Invalid_Request_Parameter_Exception In case there is no order number provided. */ public function set_metadata( array $metadata ) { $this->set_param( 'metadata', $metadata ); if ( ! isset( $metadata['order_number'] ) ) { return; // No description to generate. } // The description is based on the order number here. $description = $this->get_intent_description( $metadata['order_number'] ?? 0 ); $this->set_param( 'description', $description ); } /** * Level 3 data setter. * * @param array $level3 Level 3 data. */ public function set_level3( $level3 ) { if ( empty( $level3 ) || ! is_array( $level3 ) ) { return; } $this->set_param( 'level3', $this->fix_level3_data( $level3 ) ); } /** * Set fingerprint. * * @param string $fingerprint Fingerprint data. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_fingerprint( string $fingerprint = '' ) { $metadata = $this->get_param( 'metadata' ); $metadata = array_merge( $metadata, $this->get_fingerprint_metadata( $fingerprint ) ); $this->set_param( 'metadata', $metadata ); } /** * Set mandate. * * @param string $mandate Mandate. * * @return void */ public function set_mandate( string $mandate ) { $this->set_param( 'mandate', $mandate ); } /** * Mandate data setter. * * @param array $mandate_data Array containing details about mandate to create. * * @return void */ public function set_mandate_data( array $mandate_data ) { $this->set_param( 'mandate_data', $mandate_data ); } /** * Shipping data setter. * * @param array $shipping Shipping data. */ public function set_shipping( array $shipping ) { if ( empty( $shipping ) || ! is_array( $shipping ) ) { return; } $this->set_param( 'shipping', $shipping ); } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { return $this->api_client->deserialize_payment_intention_object_from_array( $response ); } } includes/core/server/request/class-add-account-tos-agreement.php 0000777 00000003366 15251156605 0021073 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Add_Account_Tos_Agreement. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for recording a new Terms of Service agreement. * Expected response is an array, containing a `success` flag. */ class Add_Account_Tos_Agreement extends Request { const REQUIRED_PARAMS = [ 'source', 'user_name', ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_add_account_tos_agreement'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::ACCOUNTS_API . '/tos_agreements'; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * If true, the request will be signed with the user token rather than blog token. * * @return bool */ public function should_use_user_token(): bool { return true; } /** * Sets the source (location) that the merchant agreed to the terms. * * @param string $source Location that the merchant agreed to the terms. * * @return void */ public function set_source( string $source ) { $this->set_param( 'source', $source ); } /** * Sets the user name. * * @param string $user_name The user_login of the provided user. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_user_name( string $user_name ) { $this->validate_user_name( $user_name ); $this->set_param( 'user_name', $user_name ); } } includes/core/server/request/class-list-authorizations.md 0000777 00000002351 15251156605 0017777 0 ustar 00 # `List_Authorizations` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\List_Authorizations` class is used to construct the request for listing authorizations. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-------------|-------------------------------------------|:---------:|:--------:|:-------------:| | `page` | `set_page( int $page )` | Yes | - | - | | `pagesize` | `set_page_size( int $page_size )` | Yes | - | `25` | | `sort` | `set_sort_by( string $sort )` | Yes | - | `'created'` | | `direction` | `set_sort_direction( string $direction )` | Yes | - | `'desc'` | ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_list_authorizations_request` - Arguments: `$request` ## Example: ```php $request = List_Authorizations::create(); $request->set_page( $page ); $request->set_page_size( $page_size ); $request->set_sort_by( $sort ); $request->set_sort_direction( $direction ); $request->send(); ``` includes/core/server/request/class-create-and-confirm-intention.md 0000777 00000005756 15251156605 0021422 0 ustar 00 # `Create_and_Confirm_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Create_and_Confirm_Intention` class is used to construct the request for creating and confirming an intention. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-------------------------|------------------------------------------------------|:---------:|:--------:|:-------------:| | `amount` | `set_amount( int $amount )` | Yes | Yes | - | | `capture_method` | `set_capture_method( bool $manual_capture = false )` | - | - | `'automatic'` | | `currency` | `set_currency_code( string $currency_code )` | Yes | Yes | - | | `customer` | `set_customer( string $customer_id )` | - | Yes | - | | `cvc_confirmation` | `set_cvc_confirmation( $cvc_confirmation = null )` | - | - | - | | `metadata` | `set_fingerprint( string $fingerprint = '' )` | - | Yes | - | | `mandate` | `set_mandate( string $mandate )` | - | - | - | | `description` | `set_metadata( array $metadata )` | - | - | - | | `off_session` | `set_off_session( bool $off_session = true )` | - | - | - | | `payment_method` | `set_payment_method( string $payment_method_id )` | Yes | Yes | - | | `payment_method_types` | `set_payment_method_types( array $payment_methods )` | - | - | - | | `payment_methods_types` | `set_payment_methods( array $payment_methods )` | - | - | - | | `setup_future_usage` | `setup_future_usage()` | - | - | - | ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_create_and_confirm_intent_request` - Arguments: `WCPay\Payment_Information $payment_information` ## Example: ```php $request = Create_and_Confirm_Intention::create(); $request->set_amount( $amount ); $request->set_capture_method( $manual_capture ); $request->set_currency_code( $currency_code ); $request->set_customer( $customer_id ); $request->set_cvc_confirmation( $cvc_confirmation ); $request->set_fingerprint( $fingerprint ); $request->set_mandate( $mandate ); $request->set_metadata( $metadata ); $request->set_off_session( $off_session ); $request->set_payment_method( $payment_method_id ); $request->set_payment_method_types( $payment_methods ); $request->set_payment_methods( $payment_methods ); $request->setup_future_usage(); $request->set_hook_args( $payment_information ); $request->send(); ``` includes/core/server/request/class-list-disputes.php 0000777 00000011372 15251156605 0016746 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\List_Transactions. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use Exception; use WC_Payments_API_Client; use WC_Payments_DB; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Response; use WCPay\Logger; use WP_REST_Request; /** * Request class for listing disputes. */ class List_Disputes extends Paginated { use Order_Info; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_list_disputes_request'; /** * Get api URI. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::DISPUTES_API; } /** * Fill request data from request. * * @param WP_REST_Request $request Request object. * * @return static */ public static function from_rest_request( $request ) { $wcpay_request = parent::from_rest_request( $request ); $filters = [ 'match' => $request->get_param( 'match' ), 'currency_is' => $request->get_param( 'store_currency_is' ), 'created_before' => $request->get_param( 'date_before' ), 'created_after' => $request->get_param( 'date_after' ), 'created_between' => (array) $request->get_param( 'date_between' ), 'search' => $request->get_param( 'search' ), 'status_is' => $request->get_param( 'status_is' ), 'status_is_not' => $request->get_param( 'status_is_not' ), ]; $wcpay_request->set_filters( $filters ); return $wcpay_request; } /** * Set match. * * @param string $match Match. * * @return void */ public function set_match( string $match ) { $this->set_param( 'match', $match ); } /** * Set currency is. * * @param string $currency_is Store currency. * * @return void */ public function set_currency_is( string $currency_is ) { $this->set_param( 'currency_is', $currency_is ); } /** * Set created after. * * @param string $created_after Created after. * * @return void * @throws \WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception */ public function set_created_after( string $created_after ) { $this->validate_date( $created_after ); $this->set_param( 'created_after', $created_after ); } /** * Set created before. * * @param string $created_before Created before. * * @return void * @throws \WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception */ public function set_created_before( string $created_before ) { $this->validate_date( $created_before ); $this->set_param( 'created_before', $created_before ); } /** * Set created between. * * @param array $created_between Created between. * * @return void */ public function set_created_between( array $created_between ) { if ( $created_between ) { $this->set_param( 'created_between', $created_between ); } } /** * Set search. * * @param string|string[] $search Search term or terms. * @return void * @throws Invalid_Request_Parameter_Exception Whenever the parameter is not a string or array. */ public function set_search( $search ) { if ( ! is_string( $search ) && ! is_array( $search ) ) { throw new Invalid_Request_Parameter_Exception( esc_html__( 'The search parameter must be a string, or an array of strings.', 'woocommerce-payments' ), 'wcpay_core_invalid_request_parameter_invalid_search' ); } $this->set_param( 'search', $search ); } /** * Set status is. * * @param string $status_is Status is. * * @return void */ public function set_status_is( string $status_is ) { $this->set_param( 'status_is', $status_is ); } /** * Set status is not. * * @param string $status_is_not Status is not. * * @return void */ public function set_status_is_not( string $status_is_not ) { $this->set_param( 'status_is_not', $status_is_not ); } /** * Return formatted response. * * @param mixed $response Transactions from server. * * @return Response */ public function format_response( $response ) { // Add WooCommerce order information to each dispute. if ( isset( $response['data'] ) ) { $wcpay_db = new WC_Payments_DB(); foreach ( $response['data'] as &$dispute ) { try { // Wrap with try/catch to avoid failing whole request because of a single dispute. $dispute['order'] = null; $order = $wcpay_db->order_from_charge_id( $dispute['charge_id'] ); if ( $order ) { $dispute['order'] = $this->build_order_info( $order ); } } catch ( Exception $e ) { Logger::error( 'Error adding order info to dispute ' . $dispute['dispute_id'] . ' : ' . $e->getMessage() ); continue; } } } return new Response( $response ); } } includes/core/server/request/class-request-utils.php 0000777 00000002623 15251156605 0016762 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Request_Utils. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use DateTime; use DateTimeZone; /** * WC Request Utils class */ class Request_Utils { /** * Formats the incoming transaction date as per the blog's timezone. * * @param string|null $transaction_date Transaction date to format. * @param string|null $user_timezone User's timezone passed from client. * * @return string|null The formatted transaction date as per timezone. */ public static function format_transaction_date_by_timezone( $transaction_date, $user_timezone ) { if ( is_null( $transaction_date ) || is_null( $user_timezone ) ) { return $transaction_date; } // Get blog timezone. $blog_time = new DateTime( $transaction_date ); $blog_time->setTimezone( new DateTimeZone( wp_timezone_string() ) ); // Get local timezone. $local_time = new DateTime( $transaction_date ); $local_time->setTimezone( new DateTimeZone( $user_timezone ) ); // Compute time difference in minutes. $time_difference = ( strtotime( $local_time->format( 'Y-m-d H:i:s' ) ) - strtotime( $blog_time->format( 'Y-m-d H:i:s' ) ) ) / 60; // Shift date by time difference. $formatted_date = new DateTime( $transaction_date ); date_modify( $formatted_date, $time_difference . 'minutes' ); return $formatted_date->format( 'Y-m-d H:i:s' ); } } includes/core/server/request/class-update-intention.md 0000777 00000004667 15251156605 0017246 0 ustar 00 # `Update_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Update_Intention` class is used to construct the request for updating an intention. ## Parameters When creating `Update_Intention` requests, the item ID must be provided to the `::create()` method. The identifier should be in the `pi_XXX` format. | Parameter | Setter | Immutable | Required | Default value | |--------------------------|---------------------------------------------------------------|:---------:|:--------:|:-------------:| | `amount` | `set_amount( int $amount )` | Yes | - | - | | `currency` | `set_currency_code( string $currency_code )` | - | - | - | | `customer` | `set_customer( string $customer_id )` | - | - | - | | `metadata` | `set_fingerprint( $fingerprint = '' )` | - | - | - | | `description` | `set_metadata( $metadata )` | - | - | - | | `payment_country` | `set_payment_country( string $payment_country )` | - | - | - | | `payment_method_options` | `set_payment_method_options( array $payment_method_options )` | - | - | - | | `payment_method_types` | `set_payment_method_types( array $payment_methods )` | - | - | - | | `setup_future_usage` | `setup_future_usage()` | - | - | - | ## Filter - Name: `wcpay_update_intention_request` - Arguments: `WC_Order $order` ## Example: ```php $request = Update_Intention::create( $id ); $request->set_amount( $amount ); $request->set_currency_code( $currency_code ); $request->set_customer( $customer_id ); $request->set_fingerprint( $fingerprint ); $request->set_metadata( $metadata ); $request->set_payment_country( $payment_country ); $request->set_payment_method_options( $payment_method_options ); $request->set_payment_method_types( $payment_methods ); $request->setup_future_usage(); $request->set_hook_args( $order ); $request->send(); ``` includes/core/server/request/class-woopay-create-and-confirm-intention.md 0000777 00000006667 15251156605 0022740 0 ustar 00 # `WooPay_Create_and_Confirm_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\WooPay_Create_and_Confirm_Intention` class is used to construct the request for creating and confirming an intention for WooPay. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |------------------------------|------------------------------------------------------|:---------:|:--------:|:-------------:| | `amount` | `set_amount( int $amount )` | Yes | Yes | - | | `capture_method` | `set_capture_method( bool $manual_capture = false )` | - | - | `'automatic'` | | `currency` | `set_currency_code( string $currency_code )` | Yes | Yes | - | | `customer` | `set_customer( string $customer_id )` | - | Yes | - | | `cvc_confirmation` | `set_cvc_confirmation( $cvc_confirmation = null )` | - | - | - | | `metadata` | `set_fingerprint( string $fingerprint = '' )` | - | Yes | - | | `woopay_has_subscription` | `set_has_woopay_subscription( $has = true )` | - | - | - | | `is_platform_payment_method` | `set_is_platform_payment_method( $is = true )` | - | - | - | | `mandate` | `set_mandate( string $mandate )` | - | - | - | | `description` | `set_metadata( array $metadata )` | - | - | - | | `off_session` | `set_off_session( bool $off_session = true )` | - | - | - | | `payment_method` | `set_payment_method( string $payment_method_id )` | Yes | Yes | - | | `payment_method_types` | `set_payment_method_types( array $payment_methods )` | - | - | - | | `payment_methods_types` | `set_payment_methods( array $payment_methods )` | - | - | - | | `setup_future_usage` | `setup_future_usage()` | - | - | - | ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_create_and_confirm_intent_request` - Arguments: `WCPay\Payment_Information $payment_information` ## Example: ```php $request = WooPay_Create_and_Confirm_Intention::create(); $request->set_amount( $amount ); $request->set_capture_method( $manual_capture ); $request->set_currency_code( $currency_code ); $request->set_customer( $customer_id ); $request->set_cvc_confirmation( $cvc_confirmation ); $request->set_fingerprint( $fingerprint ); $request->set_has_woopay_subscription( $has ); $request->set_is_platform_payment_method( $is ); $request->set_mandate( $mandate ); $request->set_metadata( $metadata ); $request->set_off_session( $off_session ); $request->set_payment_method( $payment_method_id ); $request->set_payment_method_types( $payment_methods ); $request->set_payment_methods( $payment_methods ); $request->setup_future_usage(); $request->set_hook_args( $payment_information ); $request->send(); ``` includes/core/server/request/class-activate-pm-promotion.md 0000777 00000002336 15251156605 0020204 0 ustar 00 # `Activate_PM_Promotion` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Activate_PM_Promotion` class is used to construct the request for activating a payment method (PM) promotion. When a promotion is activated, the associated payment method discount is applied to the merchant's account. ## Parameters When creating `Activate_PM_Promotion` requests, the promotion ID must be provided to the `::create()` method. The identifier should be in the format used by the promotions system (e.g., `klarna-2026-promo__spotlight`). There are no additional parameters for this request. ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_activate_pm_promotion_request` - Arguments: None required (the promotion ID is available via `$request->get_id()`) ## Example ```php use WCPay\Core\Server\Request\Activate_PM_Promotion; $request = Activate_PM_Promotion::create( $promotion_id ); $request->assign_hook( 'wcpay_activate_pm_promotion_request' ); $response = $request->send(); ``` ## Related - [`Get_PM_Promotions`](class-get-pm-promotions.md) - Retrieves available PM promotions from the server includes/core/server/request/class-create-intention.md 0000777 00000004431 15251156605 0017214 0 ustar 00 # `Create_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Create_Intention` class is used to construct the request for creating an intention. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |------------------------|------------------------------------------------------|:---------:|:--------:|:-------------:| | `amount` | `set_amount( int $amount )` | Yes | Yes | - | | `capture_method` | `set_capture_method( bool $manual_capture = false )` | - | - | - | | `currency` | `set_currency_code( string $currency_code )` | - | Yes | - | | `customer` | `set_customer( string $customer_id )` | - | - | - | | `metadata` | `set_fingerprint( string $fingerprint = '' )` | - | - | - | | `mandate` | `set_mandate( string $mandate )` | - | - | - | | `mandate_data` | `set_mandate_data( array $mandate_data )` | - | - | - | | `description` | `set_metadata( array $metadata )` | - | - | - | | `payment_method` | `set_payment_method( string $payment_method_id )` | - | - | - | | `payment_method_types` | `set_payment_method_types( array $payment_methods )` | - | - | - | ## Filter - Name: `wcpay_create_intent_request` - Arguments: `WC_Order $order` ## Example: ```php $request = Create_Intention::create(); $request->set_amount( $amount ); $request->set_capture_method( $manual_capture ); $request->set_currency_code( $currency_code ); $request->set_customer( $customer_id ); $request->set_fingerprint( $fingerprint ); $request->set_mandate( $mandate ); $request->set_mandate_data( $mandate_data ); $request->set_metadata( $metadata ); $request->set_payment_method( $payment_method_id ); $request->set_payment_method_types( $payment_methods ); $request->send(); ``` includes/core/server/request/class-woopay-create-intent.md 0000777 00000005066 15251156605 0020027 0 ustar 00 # `WooPay_Create_Intent` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\WooPay_Create_Intent` class is used to construct the request for creating an intention for WooPay. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-----------------------------------|-------------------------------------------------------|:---------:|:--------:|:-------------:| | `amount` | `set_amount( int $amount )` | Yes | Yes | - | | `capture_method` | `set_capture_method( bool $manual_capture = false )` | - | - | - | | `currency` | `set_currency_code( string $currency_code )` | - | Yes | - | | `customer` | `set_customer( string $customer_id )` | - | - | - | | `metadata` | `set_fingerprint( string $fingerprint = '' )` | - | - | - | | `mandate` | `set_mandate( string $mandate )` | - | - | - | | `description` | `set_metadata( array $metadata )` | - | - | - | | `payment_method` | `set_payment_method( string $payment_method_id )` | - | - | - | | `payment_method_types` | `set_payment_method_types( array $payment_methods )` | - | - | - | | `save_payment_method_to_platform` | `set_save_payment_method_to_platform( bool $toggle )` | Yes | - | - | ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_create_intent_request` - Arguments: `WC_Order $order` ## Example: ```php $request = WooPay_Create_Intent::create(); $request->set_amount( $amount ); $request->set_capture_method( $manual_capture ); $request->set_currency_code( $currency_code ); $request->set_customer( $customer_id ); $request->set_fingerprint( $fingerprint ); $request->set_mandate( $mandate ); $request->set_metadata( $metadata ); $request->set_payment_method( $payment_method_id ); $request->set_payment_method_types( $payment_methods ); $request->set_save_payment_method_to_platform( $toggle ); $request->set_hook_args( $order ); $request->send(); ``` includes/core/server/request/class-get-request.php 0000777 00000003225 15251156605 0016400 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Generic. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; /** * Generic GET WCPay Server Request. */ class Get_Request extends Request { /** * The request's API. * * @var string Check WCPay\Core\Server\APIs. */ private $api = ''; // Just to be safe in case someone misses to add it. It will be validated by default. /** * Set api route. * * @param string $api Api route. * * @throws Invalid_Request_Parameter_Exception */ public function set_api( string $api ) { $this->validate_api_route( $api ); $this->api = null === $this->id ? $api : $api . "/$this->id"; } /** * Get API route. * * @return string */ public function get_api(): string { return $this->api; } /** * Get method. * * @return string */ public function get_method(): string { return 'GET'; } /** * Override from parent class. * * @param mixed $id ID to set. * * @return void */ public function set_request_route_id_parameter( $id ) { $this->set_id( $id ); // We don't need extra validation here. } /** * Set ID: * * @param mixed $id Set id. * * @return void */ public function set_id( $id ) { $this->id = $id; } /** * Format response to array to prevent calling to_array() every time. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { return $response; } } includes/core/server/request/class-list-fraud-outcome-transactions.php 0000777 00000017043 15251156605 0022367 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\List_Fraud_Outcome_Transactions. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WC_Payments_Utils; use WC_Payments_API_Client; use WCPay\Constants\Fraud_Meta_Box_Type; use WCPay\Fraud_Prevention\Models\Rule; /** * Request class for getting intents. */ class List_Fraud_Outcome_Transactions extends Paginated { const DEFAULT_PARAMS = [ 'page' => 1, 'pagesize' => 25, 'sort' => 'date', 'direction' => 'desc', ]; /** * Fraud outcome status. * * @var string $status */ private $status; /** * Returns the request's API. * * @return string * @throws Invalid_Request_Parameter_Exception */ public function get_api(): string { $status = $this->status ?? 'null'; if ( ! Rule::is_valid_fraud_outcome_status( $status ) ) { throw new Invalid_Request_Parameter_Exception( "Invalid fraud outcome status provided: $status", 'invalid_fraud_outcome_status' ); } return WC_Payments_API_Client::FRAUD_OUTCOMES_API . '/status/' . $this->status; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'GET'; } /** * Sets the fraud outcome status. * * @param string $status Fraud outcome status. */ public function set_status( $status ) { $this->status = $status; } /** * Sets the search param. * * @param array $search Search param. */ public function set_search( $search ) { $this->set_param( 'search', $search ); } /** * Sets the search term param. * * @param string $search_term Search term param. */ public function set_search_term( $search_term ) { $this->set_param( 'search_term', $search_term ); } /** * Sets the additional status param. * * @param string $additional_status Additional status param. */ public function set_additional_status( $additional_status ) { $this->set_param( 'additional_status', $additional_status ); } /** * Used to prepare request from WP Rest data. * * @param \WP_REST_Request $request Request object. * * @return static */ public static function from_rest_request( $request ) { $wcpay_request = parent::from_rest_request( $request ); $search = $request->get_param( 'search' ) ?? []; $status = $request->get_param( 'status' ); $search_term = $request->get_param( 'search_term' ) ?? ''; $additional_status = $request->get_param( 'additional_status' ) ?? ''; $wcpay_request->set_status( $status ); $wcpay_request->set_search( $search ); $wcpay_request->set_search_term( $search_term ); $wcpay_request->set_additional_status( $additional_status ); return $wcpay_request; } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { $params = $this->get_params(); $sort = $params['sort']; $search = $params['search']; $direction = $params['direction']; if ( ! is_array( $response ) ) { return $response; } // The fraud outcomes list doesn't hold the status of the payment intent, so we need // to remove the payments that aren't in the desired status anymore. $fraud_outcomes = array_reduce( $response, function ( $result, $current ) use ( $search ) { $outcome = $this->build_fraud_outcome_transactions_order_info( $current ); // Removes the outcome that is not a valid order. if ( empty( $outcome ) ) { return $result; } // Removes the outcome that is not pending review. $is_review_pending = 'requires_capture' === $outcome['payment_intent']['status'] && empty( $outcome['manual_review'] ) && Fraud_Meta_Box_Type::REVIEW === $outcome['fraud_meta_box_type']; if ( 'review' === $this->status && ! $is_review_pending ) { return $result; } // Removes the outcome that is not blocked. $block_statuses = [ Fraud_Meta_Box_Type::BLOCK, Fraud_Meta_Box_Type::REVIEW_BLOCKED ]; $has_block_status = in_array( $outcome['fraud_meta_box_type'], $block_statuses, true ); if ( 'block' === $this->status && ! $has_block_status ) { return $result; } // Performs the search. if ( is_array( $search ) && ! empty( $search ) ) { $found = array_reduce( $search, function ( $found, $term ) use ( $outcome ) { return $this->get_search_result( $found, $term, $outcome ); }, false ); if ( ! $found ) { return $result; } } unset( $outcome['manual_review'] ); $result[] = $outcome; return $result; }, [] ); // Handles the sorting. $compare_sorting = function ( $a, $b ) use ( $sort, $direction ) { return $this->get_sort_result( $a, $b, $sort, $direction ); }; usort( $fraud_outcomes, $compare_sorting ); return $fraud_outcomes; } /** * Builds the order info for fraud outcome transaction. * * The fraud outcome result doesn't give all the data required to list the on review transactions. * So we need to fill in any empty data, such as payment_intent_id and the actual data from the payment intent. * * @param array $outcome Fraud outcome array. * * @return array|null */ private function build_fraud_outcome_transactions_order_info( $outcome ) { $order = wc_get_order( $outcome['order_id'] ); // Skip the outcome if it's not a valid order or if it's a refund. if ( empty( $order ) || is_a( $order, 'WC_Order_Refund' ) ) { return null; } $outcome['payment_intent'] = []; $outcome['payment_intent']['id'] = $outcome['payment_intent_id'] ?? $order->get_meta( '_intent_id' ) ?? $order->get_transaction_id(); $outcome['payment_intent']['status'] = $order->get_meta( '_intention_status' ); $outcome['amount'] = WC_Payments_Utils::prepare_amount( $order->get_total(), $order->get_currency() ); $outcome['currency'] = $order->get_currency(); $outcome['customer_name'] = wc_clean( $order->get_formatted_billing_full_name() ); $outcome['manual_review'] = $order->get_meta( '_wcpay_fraud_outcome_manual_entry' ); $outcome['fraud_meta_box_type'] = $order->get_meta( '_wcpay_fraud_meta_box_type' ); unset( $outcome['payment_intent_id'] ); return $outcome; } /** * Returns the result for a given search. * * @param bool $found If it was found already. * @param string $term Term to be searched. * @param array $outcome Fraud outcome object. * * @return bool */ private function get_search_result( $found, $term, $outcome ) { if ( $found ) { return $found; } // Search by order id. if ( preg_match( '/#(\d+)/', $term, $matches ) ) { return $matches[1] === (string) $outcome['order_id']; } // Search by customer name. return (bool) preg_match( "/{$term}/i", $outcome['customer_name'] ); } /** * Returns the sort result for the comparison of two items. * * @param array $a First item to be compared. * @param array $b Second item to be compared. * @param array $sort Sort key. * @param string $direction Sort direction. * * @return int */ private function get_sort_result( $a, $b, $sort, $direction ) { $key = 'date' === $sort ? 'created' : $sort; if ( ! array_key_exists( $key, $a ) || ! array_key_exists( $key, $b ) ) { return 0; } $a = $a[ $key ]; $b = $b[ $key ]; if ( $a === $b ) { return 0; } if ( 'desc' === $direction ) { return $a < $b ? 1 : -1; } return $a < $b ? -1 : 1; } } includes/core/server/request/class-cancel-intention.md 0000777 00000001250 15251156605 0017172 0 ustar 00 # `Cancel_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Cancel_Intention` class is used to construct the request for canceling an intention. ## Parameters When creating `Cancel_Intention` requests, the item ID must be provided to the `::create()` method. The identifier should be in the `pi_XXX` format. There are no additional parameters for this request. ## Filter - Name: `wcpay_cancel_intent_request` - Arguments: `WC_Order $order` ## Example: ```php $request = Cancel_Intention::create( $id ); $request->set_hook_args( $order ) $request->send(); ``` includes/core/server/request/class-capture-intention.php 0000777 00000004700 15251156605 0017602 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Capture_Intention. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for capturing intents. */ class Capture_Intention extends Request { use Intention; use Level3; const IMMUTABLE_PARAMS = [ 'amount_to_capture' ]; const REQUIRED_PARAMS = [ 'amount_to_capture' ]; const DEFAULT_PARAMS = [ 'level3' => [], ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_capture_intent_request'; /** * Sets the intent ID, which will be used in the request URL. * * @param string $id Sets the intent ID, which will be used in the request URL. * * @throws Invalid_Request_Parameter_Exception */ protected function set_id( string $id ) { $this->validate_stripe_id( $id ); $this->id = $id; } /** * Returns the request's API. * * @return string * @throws Invalid_Request_Parameter_Exception */ public function get_api(): string { return WC_Payments_API_Client::INTENTIONS_API . '/' . $this->id . '/capture'; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * Stores the amount for the intent. * * @param int $amount Amount to capture. */ public function set_amount_to_capture( int $amount ) { $this->set_param( 'amount_to_capture', $amount ); } /** * Level 3 data setter. * * @param array $level3 Level 3 data. */ public function set_level3( $level3 ) { if ( empty( $level3 ) || ! is_array( $level3 ) ) { return; } $this->set_param( 'level3', $this->fix_level3_data( $level3 ) ); } /** * Setter for intent metadata. * * @param array $metadata Intent metadata that includes stuff like order details, card reader specifics, etc.. */ public function set_metadata( array $metadata ): void { $this->set_param( 'metadata', $metadata ); } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { return $this->api_client->deserialize_payment_intention_object_from_array( $response ); } } includes/core/server/request/class-generic.php 0000777 00000006211 15251156605 0015545 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Generic. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Exceptions\Server\Request\Server_Request_Exception; use WCPay\Core\Server\Request; /** * Generic WCPay Server Request. * * This class is not extendable, if you need something specific, * it will not be generic anymore, create your own. */ final class Generic extends Request { /** * The request's API. * * @var string Check WCPay\Core\Server\APIs. */ private $api; /** * The method of the request. * * @var string See the constants in WordPress's `Requests` class. */ private $method; /** * If true, the request will be signed with the user token rather than blog token. Defaults to false. * * @var bool */ private $should_use_user_token = false; /** * Creates a new instance of the class. * * @param mixed $id The identifier for various update/get/delete requests. * * @throws Server_Request_Exception */ public static function create( $id = null ) { throw new Server_Request_Exception( 'You cannot create request this way.', 'wcpay_core_server_request_invalid_method_call' ); } /** * Instantiates the request object. * * @param string $api The API to use. See WCPay\Core\Server\APIs. * @param string $method The request method. See the `Requests` class. * @param array $parameters The parameters for the request. * @throws Invalid_Request_Parameter_Exception An exception if there are invalid properties. */ public function __construct( string $api, string $method, ?array $parameters = null ) { if ( ! defined( \WC_Payments_Utils::get_wpcore_request_class() . "::$method" ) ) { throw new Invalid_Request_Parameter_Exception( 'Invalid generic request method', 'wcpay_core_invalid_request_parameter_method_not_defined' ); } $this->api = $api; $this->method = $method; if ( ! empty( $parameters ) ) { foreach ( $parameters as $key => $value ) { $this->set( $key, $value ); } } } /** * Generic setter for parameters. * * @param string $key Key of the parameter. * @param mixed $value Value of the parameter. * @return Generic Instance of the class for method chaining. */ public function set( $key, $value ) { // Use the `Request` setter here. $this->set_param( $key, $value ); return $this; } /** * Returns the needed API. * * @return string */ public function get_api(): string { return $this->api; } /** * Returns the method of the request. * * @return string */ public function get_method(): string { return $this->method; } /** * If true, the request will be signed with the user token rather than blog token. Defaults to false. * * @return bool */ public function should_use_user_token(): bool { return $this->should_use_user_token; } /** * Sets the request to use the user token. * * @return void */ public function use_user_token() { $this->should_use_user_token = true; } } includes/core/server/request/class-get-account-capital-link.md 0000777 00000002443 15251156605 0020524 0 ustar 00 # `Get_Account_Capital_Link` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Get_Account_Capital_Link` class is used to construct the request for retrieving a one-time capital link. Expected response is an account link object with create, expires_at, and url fields. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-------------------|------------------------------------------|:---------:|:--------:|:-------------:| | `type` | `set_type( string $type )` | - | Yes | - | | `return_url` | `set_return_url( string $return_url )` | - | Yes | - | | `refresh_url` | `set_refresh_url( string $refresh_url )` | - | Yes | - | ## Filter - Name: `wcpay_get_account_capital_link` - Arguments: None. ## Example: ```php $request = Get_Account_Capital_Link::create(); $request->set_type( 'capital_financing_offer' ); $request->set_return_url( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' ); $request->set_refresh_url( 'http://example.org/wp-admin/admin.php?wcpay-loan-offer' ); $request->send(); ``` includes/core/server/request/class-get-setup-intention.md 0000777 00000001176 15251156605 0017671 0 ustar 00 # `Get_Setup_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Get_Setup_Intention` class is used to construct the request for retrieving a setup intention. ## Parameters When creating `Get_Setup_Intention` requests, the item ID must be provided to the `::create()` method. The identifier should be in the `seti_XXX` format. There are no additional parameters for this request. ## Filter - Name: `wcpay_get_setup_intent_request` ## Example: ```php $request = Get_Setup_Intention::create( $id ); $request->send(); ``` includes/core/server/request/class-list-charge-refunds.php 0000777 00000003404 15251156605 0020000 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\List_Charge_Refunds. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for charge refunds. */ class List_Charge_Refunds extends Request { const DEFAULT_PARAMS = [ 'limit' => 100, ]; const IMMUTABLE_PARAMS = [ 'charge' ]; const REQUIRED_PARAMS = [ 'charge' ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_list_charge_refunds_request'; /** * Sets the charge ID, which will be used in the request URL. * * @param string $charge_id Sets the charge ID, which will be used in the request URL. * * @throws Invalid_Request_Parameter_Exception */ public function set_charge( string $charge_id ) { /** * `py_XYZ` objects are identical to charges, and sometimes occur * whenever the payment was made in a non-deposit currency. */ $this->validate_stripe_id( $charge_id, [ 'ch', 'py' ] ); $this->set_param( 'charge', $charge_id ); } /** * Stores the limit for the lists. * * @param int $limit Number to limit results. * @throws Invalid_Request_Parameter_Exception */ public function set_limit( int $limit ) { $this->validate_is_larger_than( $limit, 0 ); $this->set_param( 'limit', $limit ); } /** * Returns the request's API. * * @return string * @throws Invalid_Request_Parameter_Exception */ public function get_api(): string { return WC_Payments_API_Client::REFUNDS_API; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'GET'; } } includes/core/server/request/class-list-deposits.php 0000777 00000004355 15251156605 0016743 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\List_Deposits. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments_API_Client; use WP_REST_Request; /** * Request class for paginated requests. */ class List_Deposits extends Paginated { use Date_Parameters; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_list_deposits_request'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::DEPOSITS_API; } /** * Used to prepare request from WP Rest data. * * @param WP_REST_Request $request Request object. * * @return static */ public static function from_rest_request( $request ) { $wcpay_request = parent::from_rest_request( $request ); $filters = [ 'match' => $request->get_param( 'match' ), 'store_currency_is' => $request->get_param( 'store_currency_is' ), 'date_before' => $request->get_param( 'date_before' ), 'date_after' => $request->get_param( 'date_after' ), 'date_between' => (array) $request->get_param( 'date_between' ), 'status_is' => $request->get_param( 'status_is' ), 'status_is_not' => $request->get_param( 'status_is_not' ), ]; $wcpay_request->set_filters( $filters ); return $wcpay_request; } /** * Set match. * * @param string $match Match. * * @return void */ public function set_match( string $match ) { $this->set_param( 'match', $match ); } /** * Set currency is. * * @param string $store_currency_is Store currency. * * @return void */ public function set_store_currency_is( string $store_currency_is ) { $this->set_param( 'store_currency_is', $store_currency_is ); } /** * Set status is. * * @param string $status_is Status is. * * @return void */ public function set_status_is( string $status_is ) { $this->set_param( 'status_is', $status_is ); } /** * Set status is not. * * @param string $status_is_not Status is not. * * @return void */ public function set_status_is_not( string $status_is_not ) { $this->set_param( 'status_is_not', $status_is_not ); } } includes/core/server/request/class-get-pm-promotions.md 0000777 00000003411 15251156605 0017341 0 ustar 00 # `Get_PM_Promotions` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Get_PM_Promotions` class is used to construct the request for retrieving available payment method (PM) promotions from the WooCommerce Payments server. These promotions offer merchants discounts on processing fees for specific payment methods. ## Parameters No ID is required for creation. Store context can be provided via the `set_store_context_params()` method. ### Available Methods | Method | Description | |--------|-------------| | `set_store_context_params( array $context )` | Attaches store context (dismissals, locale, etc.) to the request | ### Store Context Parameters The `set_store_context_params()` method accepts an array with the following keys: | Key | Type | Description | |-----|------|-------------| | `dismissals` | `array` | Map of dismissed promotion IDs to timestamps | | `locale` | `string` | Store locale (e.g., `en_US`) | ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_get_pm_promotions_request` - Arguments: None required ## Response This request returns the raw response (including headers) to allow access to caching directives like `cache-for`. ## Example ```php use WCPay\Core\Server\Request\Get_PM_Promotions; $store_context = [ 'dismissals' => [ 'promo1__spotlight' => 1234567890 ], 'locale' => get_locale(), ]; $request = Get_PM_Promotions::create(); $request->set_store_context_params( $store_context ); $request->assign_hook( 'wcpay_get_pm_promotions_request' ); $response = $request->handle_rest_request(); ``` ## Related - [`Activate_PM_Promotion`](class-activate-pm-promotion.md) - Activates a PM promotion includes/core/server/request/class-list-charge-refunds.md 0000777 00000001627 15251156605 0017616 0 ustar 00 # `List_Charge_Refunds` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\List_Charge_Refunds` class is used to construct the request for listing refunds of a specific charge. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-----------|--------------------------------|:---------:|:--------:|:-------------:| | `charge` | `set_charge( string $charge_id )` | Yes | Yes | None | | `limit` | `set_limit( int $limit )` | No | No | 100 | ## Filter - Name: `wcpay_list_charge_refunds_request` - Arguments: None ## Example: ```php $request = List_Charge_Refunds::create(); $request->set_charge( 'ch_id' ); $request->set_limit( 100 ); // It is not required. You can also skip this setter. $request->send(); ``` includes/core/server/request/class-woopay-create-and-confirm-intention.php 0000777 00000002375 15251156605 0023117 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Create_And_Confirm_Intention. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; /** * Request class for creating intents. */ class WooPay_Create_And_Confirm_Intention extends Create_And_Confirm_Intention { const DEFAULT_PARAMS = [ 'is_platform_payment_method' => false, 'woopay_has_subscription' => false, 'save_payment_method_to_platform' => false, ]; /** * Toggles the flag, which indicates that this is a platform payment method. * * @param bool $is Whether it is indeed a platform payment method (Optional). */ public function set_is_platform_payment_method( $is = true ) { $this->set_param( 'is_platform_payment_method', $is ); } /** * Toggles the flag, which indicates that there is a WooPay subscription. * * @param bool $has Whether there is a subscription (Optional). */ public function set_has_woopay_subscription( $has = true ) { $this->set_param( 'woopay_has_subscription', $has ); } /** * Save payment method to platform. * * @param bool $save save payment method to platform. */ public function set_save_payment_method_to_platform( $save = true ) { $this->set_param( 'save_payment_method_to_platform', $save ); } } includes/core/server/request/class-list-authorizations.php 0000777 00000001156 15251156605 0020170 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\List_Authorizations. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for listing authorizations. */ class List_Authorizations extends Paginated { /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_list_authorizations_request'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::AUTHORIZATIONS_API; } } includes/core/server/request/class-update-intention.php 0000777 00000011733 15251156605 0017425 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Update_Intent. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for creating intents. */ class Update_Intention extends Request { use Intention; use Level3; const IMMUTABLE_PARAMS = [ 'amount' ]; const DEFAULT_PARAMS = [ 'receipt_email' => '', 'metadata' => [], ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_update_intention_request'; /** * Sets the intent ID, which will be used in the request URL. * * @param string $id Sets the intent ID, which will be used in the request URL. * * @throws Invalid_Request_Parameter_Exception */ protected function set_id( string $id ) { $this->validate_stripe_id( $id ); $this->id = $id; } /** * Returns the request's API. * * @return string * @throws Invalid_Request_Parameter_Exception */ public function get_api(): string { return WC_Payments_API_Client::INTENTIONS_API . '/' . $this->id; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * If the payment method should be saved to the store, this enables future usage. */ public function setup_future_usage() { $this->set_param( 'setup_future_usage', 'off_session' ); } /** * Customer setter. * * @param string $customer_id ID of the customer making the payment. * @return void * * @throws Invalid_Request_Parameter_Exception */ public function set_customer( string $customer_id ) { $this->validate_stripe_id( $customer_id, 'cus' ); $this->set_param( 'customer', $customer_id ); } /** * Stores the amount for the intent. * * @param int $amount The amount in ToDo units. * @throws Invalid_Request_Parameter_Exception */ public function set_amount( int $amount ) { $this->validate_is_larger_than( $amount, 0 ); $this->set_param( 'amount', $amount ); } /** * Currency code setter. * * @param string $currency_code Currency to charge in. * @throws Invalid_Request_Parameter_Exception When the currency code is invalid. */ public function set_currency_code( string $currency_code ) { $this->validate_currency_code( $currency_code ); $this->set_param( 'currency', $currency_code ); } /** * Set/update payment methods. * * @param array $payment_methods Payment methods. * * @return void */ public function set_payment_method_types( array $payment_methods ) { if ( ! empty( $payment_methods ) ) { $this->set_param( 'payment_method_types', $payment_methods ); } } /** * Set payment country. * * @param string $payment_country Set payment country. * * @return void * @throws \Exception */ public function set_payment_country( string $payment_country ) { if ( $payment_country && ! WC_Payments::mode()->is_dev() ) { // Do not update on dev mode, Stripe tests cards don't return the appropriate country. $this->set_param( 'payment_country', $payment_country ); } } /** * Metadata setter. * * @param array $metadata Meta data values to be sent along with payment intent creation. */ public function set_metadata( $metadata ) { // The description is based on the order number here. $description = $this->get_intent_description( $metadata['order_number'] ?? 0 ); $this->set_param( 'description', $description ); // Combine the metadata with the fingerprint. $metadata = array_merge( $metadata, $this->get_fingerprint_metadata() ); $this->set_param( 'metadata', $metadata ); } /** * Set fingerprint. * * @param string $fingerprint Fingerprint data. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_fingerprint( $fingerprint = '' ) { $metadata = $this->get_param( 'metadata' ); $metadata = array_merge( $metadata, $this->get_fingerprint_metadata( $fingerprint ) ); $this->set_param( 'metadata', $metadata ); } /** * Level 3 data setter. * * @param array $level3 Level 3 data. */ public function set_level3( $level3 ) { if ( empty( $level3 ) || ! is_array( $level3 ) ) { return; } $this->set_param( 'level3', $this->fix_level3_data( $level3 ) ); } /** * Set payment method options. * * @param array $payment_method_options Payment method options. * * @return void */ public function set_payment_method_options( array $payment_method_options ) { $this->set_param( 'payment_method_options', $payment_method_options ); } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { return $this->api_client->deserialize_payment_intention_object_from_array( $response ); } } includes/core/server/request/class-get-pm-promotions.php 0000777 00000003355 15251156605 0017537 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Get_PM_Promotions. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments_API_Client; use WCPay\Core\Server\Request; /** * GET WCPay payment method (PM) promotions from the Transact Platform. */ class Get_PM_Promotions extends Request { /** * Specifies the WordPress hook name that will be triggered upon making the request. * * @var string */ protected $hook = 'wcpay_get_pm_promotions_request'; /** * Get API route. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::PROMOTIONS_API; } /** * Get method. * * @return string */ public function get_method(): string { return 'GET'; } /** * We want the raw response so we can look at the response headers. * * @return bool */ public function should_return_raw_response(): bool { return true; } /** * Attaches store context details to the request. * * @param array $context The store context to send along with the request. */ public function set_store_context_params( array $context ): void { // Go through each context entry, validate, and set as param. foreach ( $context as $key => $value ) { // If the key is not a string, skip it. if ( ! is_string( $key ) ) { continue; } // If the value is null or empty, skip it. if ( is_null( $value ) || '' === $value ) { continue; } // JSON encode arrays (like dismissals) as the server expects strings. if ( is_array( $value ) ) { $encoded = wp_json_encode( $value ); // Skip this parameter if encoding fails. if ( false === $encoded ) { continue; } $value = $encoded; } $this->set_param( $key, $value ); } } } includes/core/server/request/class-add-account-tos-agreement.md 0000777 00000001707 15251156605 0020701 0 ustar 00 # `Add_Account_Tos_Agreement` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Add_Account_Tos_Agreement` class is used to construct the request for recording a new Terms of Service agreement. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |---------------|------------------------------------------|:---------:|:--------:|:-------------:| | `source` | `set_source( string $source )` | - | Yes | - | | `user_name` | `user_name( string $user_name )` | - | Yes | - | ## Filter - Name: `wcpay_add_account_tos_agreement` - Arguments: None. ## Example: ```php $request = Add_Account_Tos_Agreement::create(); $request->set_source( 'settings-popup' ); $request->set_user_name( 'current_username' ); $request->send(); ``` includes/core/server/request/trait-use-test-mode-only-when-test-mode-onboarding.php 0000777 00000001172 15251156605 0024600 0 ustar 00 <?php /** * Trait file for WCPay\Core\Server\Request\Use_Test_Mode_Only_When_Test_Mode_Onboarding. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; /** * Trait for requests that we send the test_mode flag only when the site is onboarding in test mode. */ trait Use_Test_Mode_Only_When_Test_Mode_Onboarding { /** * Set the test_mode parameter to true only if in dev mode. * * @return array */ public static function get_default_params() { $params = parent::get_default_params(); $params['test_mode'] = \WC_Payments::mode()->is_test_mode_onboarding(); return $params; } } includes/core/server/request/class-list-documents.php 0000777 00000003566 15251156605 0017115 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\List_Documents. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments_API_Client; use WP_REST_Request; /** * Request class for listing documents. */ class List_Documents extends Paginated { use Date_Parameters; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_list_documents_request'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::DOCUMENTS_API; } /** * Used to prepare request from WP Rest data. * * @param WP_REST_Request $request Request object. * * @return static */ public static function from_rest_request( $request ) { $wcpay_request = parent::from_rest_request( $request ); $filters = [ 'match' => $request->get_param( 'match' ), 'date_before' => $request->get_param( 'date_before' ), 'date_after' => $request->get_param( 'date_after' ), 'date_between' => (array) $request->get_param( 'date_between' ), 'type_is' => $request->get_param( 'type_is' ), 'type_is_not' => $request->get_param( 'type_is_not' ), ]; $wcpay_request->set_filters( $filters ); return $wcpay_request; } /** * Set match. * * @param string $match Match. * * @return void */ public function set_match( string $match ) { $this->set_param( 'match', $match ); } /** * Set type is. * * @param string $type_is Type is. * * @return void */ public function set_type_is( string $type_is ) { $this->set_param( 'type_is', $type_is ); } /** * Set type is not. * * @param string $type_is_not Type is not. * * @return void */ public function set_type_is_not( string $type_is_not ) { $this->set_param( 'type_is_not', $type_is_not ); } } includes/core/server/request/class-get-setup-intention.php 0000777 00000003214 15251156605 0020053 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Get_Setup_Intention. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments_API_Setup_Intention; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for getting setup intents. */ class Get_Setup_Intention extends Request { /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_get_setup_intent_request'; /** * Sets the intent ID, which will be used in the request URL. * * @param string $setup_intent_id Sets the setup intent ID, which will be used in the request URL. * * @throws Invalid_Request_Parameter_Exception */ protected function set_id( string $setup_intent_id ) { $this->validate_stripe_id( $setup_intent_id ); $this->id = $setup_intent_id; } /** * Returns the request's API. * * @return string * @throws Invalid_Request_Parameter_Exception */ public function get_api(): string { return WC_Payments_API_Client::SETUP_INTENTS_API . '/' . $this->id; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'GET'; } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return WC_Payments_API_Setup_Intention Either the same response, or the correct object. */ public function format_response( $response ) { return $this->api_client->deserialize_setup_intention_object_from_array( $response ); } } includes/core/server/request/trait-date-parameters.php 0000777 00000002635 15251156605 0017233 0 ustar 00 <?php /** * Trait file for WCPay\Core\Server\Request\Date_Parameters. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore; use WC_Order; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; /** * Tait for request date parameters. */ trait Date_Parameters { /** * Set date after. * * @param string $date_after Date after. * * @return void * @throws \WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception */ public function set_date_after( string $date_after ) { $this->validate_date( $date_after ); $this->set_param( 'date_after', $date_after ); } /** * Set date before. * * @param string $date_before Date before. * * @return void * @throws \WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception */ public function set_date_before( string $date_before ) { $this->validate_date( $date_before ); $this->set_param( 'date_before', $date_before ); } /** * Set date between. * * @param array $date_between Date between. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function set_date_between( array $date_between ) { if ( ! empty( $date_between ) ) { foreach ( $date_between as $date ) { $this->validate_date( $date ); } $this->set_param( 'date_between', $date_between ); } } } includes/core/server/request/trait-intention.php 0000777 00000003671 15251156605 0016165 0 ustar 00 <?php /** * Trait file for WCPay\Core\Server\Request\Intention. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Fraud_Prevention\Buyer_Fingerprinting_Service; /** * Trait for intention helpers. */ trait Intention { /** * Returns a list of fingerprinting metadata to attach to order. * * @param string $fingerprint Fingerprint data. * * @return array List of fingerprinting metadata. */ private function get_fingerprint_metadata( $fingerprint = '' ): array { $customer_fingerprint_metadata = Buyer_Fingerprinting_Service::get_instance()->get_hashed_data_for_customer( $fingerprint ); $customer_fingerprint_metadata['fraud_prevention_data_available'] = true; return $customer_fingerprint_metadata; } /** * Returns a formatted intention description. * * @param string $order_number The order number (might be different from the ID). * @return string A formatted intention description. */ private function get_intent_description( $order_number ): string { $domain_name = str_replace( [ 'https://', 'http://' ], '', get_site_url() ); $blog_id = $this->get_blog_id(); // Forgo i18n as this is only visible in the Stripe dashboard. return sprintf( 'Online Payment%s for %s%s', 0 !== $order_number ? " for Order #$order_number" : '', $domain_name, null !== $blog_id ? " blog_id $blog_id" : '' ); } /** * Gets the current WP.com blog ID, if the Jetpack connection has been set up. * * @return integer|NULL Current WPCOM blog ID, or NULL if not connected yet. */ private function get_blog_id() { return $this->is_server_connected() ? $this->http_interface->get_blog_id() : null; } /** * Whether the site can communicate with the WCPay server (i.e. Jetpack connection has been established). * * @return bool */ private function is_server_connected() { return $this->http_interface->is_connected(); } } includes/core/server/request/class-get-account.md 0000777 00000001027 15251156605 0016153 0 ustar 00 # `Get_Account` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md). ## Description The `WCPay\Core\Server\Request\Get_Account` class is used to construct the request for retrieving account data. Note that this request sends the test_mode flag only when the site is in sandbox mode. ## Parameters None. ## Filter When using this request, provide the following filter: - Name: `wcpay_get_account`. ## Example: ```php $request = Get_Account::create(); $request->send(); ``` includes/core/server/request/class-update-account.php 0000777 00000015355 15251156605 0017056 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Update_Account. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for updating account. */ class Update_Account extends Request { /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_update_account_settings'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::ACCOUNTS_API; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * If true, the request will be signed with the user token rather than blog token. * * @return bool */ public function should_use_user_token(): bool { return true; } /** * Used to prepare request from array account_settings. * * @param array $account_settings Account settings: key is the param name, value is the param value. * * @return static * @throws Invalid_Request_Parameter_Exception When either no account settings provided or no existing setter for provided parameter. */ public static function from_account_settings( array $account_settings ) { if ( 0 === count( $account_settings ) ) { throw new Invalid_Request_Parameter_Exception( esc_html__( 'No account settings provided', 'woocommerce-payments' ), 'wcpay_core_invalid_request_parameter_account_settings_empty' ); } $wcpay_request = static::create(); foreach ( $account_settings as $param_name => $value ) { $param_setter = 'set_' . $param_name; if ( method_exists( static::class, $param_setter ) ) { $wcpay_request->{$param_setter}( $value ); } } return $wcpay_request; } /** * Sets the account statement descriptor. * * @param string $statement_descriptor Statement descriptor. * * @return void */ public function set_statement_descriptor( string $statement_descriptor ) { $this->set_param( 'statement_descriptor', $statement_descriptor ); } /** * Sets the account statement descriptor kanji. * * @param string $statement_descriptor_kanji Statement descriptor kanji. * * @return void */ public function set_statement_descriptor_kanji( string $statement_descriptor_kanji ) { $this->set_param( 'statement_descriptor_kanji', $statement_descriptor_kanji ); } /** * Sets the account statement descriptor kana. * * @param string $statement_descriptor_kana Statement descriptor kana. * * @return void */ public function set_statement_descriptor_kana( string $statement_descriptor_kana ) { $this->set_param( 'statement_descriptor_kana', $statement_descriptor_kana ); } /** * Sets the account business name. * * @param string $business_name Business name. * * @return void */ public function set_business_name( string $business_name ) { $this->set_param( 'business_name', $business_name ); } /** * Sets the account business url. * * @param string $business_url Business url. * * @return void */ public function set_business_url( string $business_url ) { $this->set_param( 'business_url', $business_url ); } /** * Sets the account business support address. * * @param string $business_support_address Business support address. * * @return void */ public function set_business_support_address( string $business_support_address ) { $this->set_param( 'business_support_address', $business_support_address ); } /** * Sets the account business support email. * * @param string $business_support_email Business support email. * * @return void */ public function set_business_support_email( string $business_support_email ) { $this->set_param( 'business_support_email', $business_support_email ); } /** * Sets the account business support phone. * * @param string $business_support_phone Business support phone. * * @return void */ public function set_business_support_phone( string $business_support_phone ) { $this->set_param( 'business_support_phone', $business_support_phone ); } /** * Sets the account branding logo. * * @param string $branding_logo Branding logo. * * @return void */ public function set_branding_logo( string $branding_logo ) { $this->set_param( 'branding_logo', $branding_logo ); } /** * Sets the account branding icon. * * @param string $branding_icon Branding icon. * * @return void */ public function set_branding_icon( string $branding_icon ) { $this->set_param( 'branding_icon', $branding_icon ); } /** * Sets the account branding primary color. * * @param string $branding_primary_color Branding primary color. * * @return void */ public function set_branding_primary_color( string $branding_primary_color ) { $this->set_param( 'branding_primary_color', $branding_primary_color ); } /** * Sets the account branding secondary color. * * @param string $branding_secondary_color Branding secondary color. * * @return void */ public function set_branding_secondary_color( string $branding_secondary_color ) { $this->set_param( 'branding_secondary_color', $branding_secondary_color ); } /** * Sets the deposit schedule interval. * * @param string $deposit_schedule_interval Deposit schedule interval. * * @return void */ public function set_deposit_schedule_interval( string $deposit_schedule_interval ) { $this->set_param( 'deposit_schedule_interval', $deposit_schedule_interval ); } /** * Sets the deposit schedule weekly anchor. * * @param string $deposit_schedule_weekly_anchor Deposit schedule weekly anchor. * * @return void */ public function set_deposit_schedule_weekly_anchor( string $deposit_schedule_weekly_anchor ) { $this->set_param( 'deposit_schedule_weekly_anchor', $deposit_schedule_weekly_anchor ); } /** * Sets the deposit schedule monthly anchor. * * @param string $deposit_schedule_monthly_anchor Deposit schedule monthly anchor. * * @return void */ public function set_deposit_schedule_monthly_anchor( string $deposit_schedule_monthly_anchor ) { $this->set_param( 'deposit_schedule_monthly_anchor', $deposit_schedule_monthly_anchor ); } /** * Sets the account locale. * * @param string $locale Account locale. * * @return void */ public function set_locale( string $locale ) { $this->set_param( 'locale', $locale ); } /** * Sets the communications email. * * @param string $communications_email Communications email. * * @return void */ public function set_communications_email( string $communications_email ) { $this->set_param( 'communications_email', $communications_email ); } } includes/core/server/request/trait-order-info.php 0000777 00000003125 15251156605 0016214 0 ustar 00 <?php /** * Trait file for WCPay\Core\Server\Request\Order_info. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore; use WC_Order; /** * Tait for getting order data or order info. */ trait Order_Info { /** * Creates the array representing order for frontend. * * @param WC_Order $order The order. * @return array */ private function build_order_info( WC_Order $order ): array { $order_info = [ 'number' => $order->get_order_number(), 'url' => $order->get_edit_order_url(), 'customer_url' => $this->get_customer_url( $order ), ]; if ( function_exists( 'wcs_get_subscriptions_for_order' ) ) { $order_info['subscriptions'] = []; $subscriptions = wcs_get_subscriptions_for_order( $order, [ 'order_type' => [ 'parent', 'renewal' ] ] ); foreach ( $subscriptions as $subscription ) { $order_info['subscriptions'][] = [ 'number' => $subscription->get_order_number(), 'url' => $subscription->get_edit_order_url(), ]; } } return $order_info; } /** * Generates url to single customer in analytics table. * * @param WC_Order $order The Order. * @return string|null */ private function get_customer_url( WC_Order $order ) { $customer_id = DataStore::get_existing_customer_id_from_order( $order ); if ( ! $customer_id ) { return null; } return add_query_arg( [ 'page' => 'wc-admin', 'path' => '/customers', 'filter' => 'single_customer', 'customers' => $customer_id, ], 'admin.php' ); } } includes/core/server/request/class-get-account.php 0000777 00000001403 15251156605 0016340 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Get_Account. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for getting account data. */ class Get_Account extends Request { use Use_Test_Mode_Only_When_Test_Mode_Onboarding; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_get_account'; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::ACCOUNTS_API; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'GET'; } } includes/core/server/request/class-create-and-confirm-setup-intention.md 0000777 00000003666 15251156605 0022556 0 ustar 00 # `Create_and_Confirm_Setup_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Create_and_Confirm_Setup_Intention` class is used to construct the request for creating and confirming a setup intention. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-------------------------|-------------------------------------------------------------|:---------:|:--------:|:-------------:| | `customer` | `set_customer( string $customer_id )` | Yes | Yes | - | | `description` | `set_metadata( array $metadata )` | - | - | - | | `payment_method` | `set_payment_method( string $payment_method_id )` | - | - | - | | `payment_method_types` | `set_payment_method_types( string $payment_method_types )` | - | - | - | | `mandata_data` | `set_mandate_data( array $mandate_data )` | - | - | - | ## Filter - Name: `wcpay_create_and_confirm_setup_intent_request` - Arguments: - `WCPay\Payment_Information $payment_information` - `bool $save_in_platform_account` - `bool $save_payment_method_to_platform` ## Example: ```php $request = Create_and_Confirm_Setup_Intention::create(); $request->set_customer( $customer_id ); $request->set_metadata( $metadata ); $request->set_payment_method( $payment_method_id ); $request->set_payment_method_types( $payment_method_types ); $request->set_mandate_data( $mandate_data ); $request->assign_hook( 'wcpay_create_and_confirm_setup_intent_request' ); $request->set_hook_args( $payment_information, $save_in_platform_account, $save_payment_method_to_platform ); $request->send(); ``` includes/core/server/request/class-capture-intention.md 0000777 00000002044 15251156605 0017412 0 ustar 00 # `Capture_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Capture_Intention` class is used to construct the request for capturing an intention. ## Parameters When creating `Capture_Intention` requests, the item ID must be provided to the `::create()` method. The identifier should be in the `pi_XXX` format. | Parameter | Setter | Immutable | Required | Default value | |---------------------|----------------------------------------|:---------:|:--------:|:-------------:| | `amount_to_capture` | `set_amount_to_capture( int $amount )` | Yes | Yes | - | ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_capture_intent_request` - Arguments: `WC_Order $order` ## Example: ```php $request = Capture_Intention::create( $id ); $request->set_amount_to_capture( $amount ); $request->set_hook_args( $order ); $request->send(); ``` includes/core/server/request/class-list-disputes.md 0000777 00000004631 15251156605 0016557 0 ustar 00 # `List_Disputes` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\List_Disputes` class is used to construct the request for listing disputes. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-------------------|-------------------------------------------------|:---------:|:--------:|:-------------:| | `created_after` | `set_created_after( string $created_after )` | - | - | - | | `created_before` | `set_created_before( string $created_before )` | - | - | - | | `created_between` | `set_created_between( array $created_between )` | - | - | - | | `currency_is` | `set_currency_is( string $currency_is )` | - | - | - | | `match` | `set_match( string $match )` | - | - | - | | `page` | `set_page( int $page )` | Yes | - | - | | `pagesize` | `set_page_size( int $page_size )` | Yes | - | `25` | | `search` | `set_search( string $search )` | - | - | - | | `sort` | `set_sort_by( string $sort )` | Yes | - | `'created'` | | `direction` | `set_sort_direction( string $direction )` | Yes | - | `'desc'` | | `status_is` | `set_status_is( string $status_is )` | - | - | - | | `status_is_not` | `set_status_is_not( string $status_is_not )` | - | - | - | ## Filter - Name: `wcpay_list_disputes_request` - Arguments: `$request` ## Example: ```php $request = List_Disputes::create(); $request->set_created_after( $created_after ); $request->set_created_before( $created_before ); $request->set_created_between( $created_between ); $request->set_currency_is( $currency_is ); $request->set_match( $match ); $request->set_page( $page ); $request->set_page_size( $page_size ); $request->set_search( $search ); $request->set_sort_by( $sort ); $request->set_sort_direction( $direction ); $request->set_status_is( $status_is ); $request->set_status_is_not( $status_is_not ); $request->send(); ``` includes/core/server/request/class-get-account-login-data.md 0000777 00000001716 15251156605 0020175 0 ustar 00 # `Get_Account_Login_Data` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Get_Account_Login_Data` class is used to construct the request for getting one-time dashboard login url. Note that this request sends the test_mode flag only when the site is in sandbox mode. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |-------------------|--------------------------------------------|:---------:|:--------:|:-------------:| | `redirect_url` | `set_redirect_url( string $redirect_url )` | - | Yes | - | ## Filter - Name: `wpcay_get_account_login_data` - Arguments: None. ## Example: ```php $request = Get_Account_Login_Data::create(); $request->set_redirect_url( 'http://example.org/wp-admin/admin.php?page=wc-admin&path=/payments/overview' ); $request->send(); ``` includes/core/server/request/class-woopay-create-intent.php 0000777 00000001160 15251156605 0020205 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\WooPay_Create_Intent. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; /** * Extended create intent request for WooPay. */ class WooPay_Create_Intent extends Create_Intention { const IMMUTABLE_PARAMS = [ 'save_payment_method_to_platform' ]; /** * Indicates whether the payment method needs to be stored with the platform. * * @param bool $toggle Whether to toggle the flag. */ public function set_save_payment_method_to_platform( bool $toggle ) { $this->set_param( 'save_payment_method_to_platform', $toggle ); } } includes/core/server/request/class-get-intention.md 0000777 00000001340 15251156605 0016524 0 ustar 00 # `Get_Intention` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\Get_Intention` class is used to construct the request for retrieving an intention. ## Parameters When creating `Get_Intention` requests, the item ID must be provided to the `::create()` method. The identifier should be in the `pi_XXX` format. There are no additional parameters for this request. ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_get_intent_request` - Arguments: `WC_Order $order` ## Example: ```php $request = Get_Intention::create( $id ); $request->set_hook_args( $order ) $request->send(); ``` includes/core/server/request/class-refund-charge.php 0000777 00000004615 15251156605 0016651 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Refund_Charge. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for refund charge. */ class Refund_Charge extends Request { const DEFAULT_PARAMS = [ 'amount' => null, 'reason' => null, ]; const IMMUTABLE_PARAMS = [ 'charge' ]; const REQUIRED_PARAMS = [ 'charge' ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_refund_charge_request'; /** * Sets the charge ID, which will be used in the request URL. * * @param string $charge_id Sets the charge ID, which will be used in the request URL. * * @throws Invalid_Request_Parameter_Exception */ public function set_charge( string $charge_id ) { /** * `py_XYZ` objects are identical to charges, and sometimes occur * whenever the payment was made in a non-deposit currency. */ $this->validate_stripe_id( $charge_id, [ 'ch', 'py' ] ); $this->set_param( 'charge', $charge_id ); } /** * Stores the amount for the refund. * * @param int $amount The amount to refund. * @throws Invalid_Request_Parameter_Exception */ public function set_amount( int $amount ) { $this->validate_is_larger_than( $amount, 0 ); $this->set_param( 'amount', $amount ); } /** * Sets the reason for the refund. * * @param string|null $reason The reason for the refund. * @throws Invalid_Request_Parameter_Exception */ public function set_reason( ?string $reason ) { $this->set_param( 'reason', $reason ); } /** * Sets the refund source describing where it was initiated from. * * @param string $source The reason for the refund. * @throws Invalid_Request_Parameter_Exception */ public function set_source( string $source ) { $this->set_param( 'metadata', array_merge( $this->get_params()['metadata'] ?? [], [ 'refund_source' => $source ] ) ); } /** * Returns the request's API. * * @return string * @throws Invalid_Request_Parameter_Exception */ public function get_api(): string { return WC_Payments_API_Client::REFUNDS_API; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } } includes/core/server/request/class-get-charge.php 0000777 00000003324 15251156605 0016141 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Get_Charge. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for getting intents. */ class Get_Charge extends Request { /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_get_charge_request'; /** * Sets the intent ID, which will be used in the request URL. * * @param string $id Sets the intent ID, which will be used in the request URL. * * @throws Invalid_Request_Parameter_Exception */ protected function set_id( string $id ) { /** * `py_XYZ` objects are identical to charges, and sometimes occur * whenever the payment was made in a non-deposit currency. */ $this->validate_stripe_id( $id, [ 'ch', 'py' ] ); $this->id = $id; } /** * Returns the request's API. * * @return string * @throws Invalid_Request_Parameter_Exception */ public function get_api(): string { return WC_Payments_API_Client::CHARGES_API . '/' . $this->id; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'GET'; } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { if ( is_wp_error( $response ) ) { return $response; } return $this->api_client->add_additional_info_to_charge( $response ); } } includes/core/server/request/class-paginated.php 0000777 00000005104 15251156605 0016065 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Paginated. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WCPay\Core\Server\Request; use WP_REST_Request; /** * Request class for paginated requests. */ abstract class Paginated extends Request { const DEFAULT_PARAMS = [ 'page' => 0, 'pagesize' => 25, 'sort' => 'created', 'direction' => 'desc', 'limit' => 100, ]; const IMMUTABLE_PARAMS = [ 'page', 'pagesize', 'sort', 'direction', 'limit', ]; /** * Returns the request's HTTP method. */ public function get_method(): string { return 'GET'; } /** * Used to prepare request from WP Rest data. * * @param WP_REST_Request $request Request object. * * @return static */ public static function from_rest_request( $request ) { $wcpay_request = static::create(); $wcpay_request->set_page( (int) $request->get_param( 'page' ) ); $wcpay_request->set_page_size( (int) ( $request->get_param( 'pagesize' ) ?? 25 ) ); $sort = $request->get_param( 'sort' ); if ( null !== $sort ) { $wcpay_request->set_sort_by( (string) $sort ); } $direction = $request->get_param( 'direction' ); if ( null !== $direction ) { $wcpay_request->set_sort_direction( (string) $direction ); } return $wcpay_request; } /** * Set filters. * * @param array $filters Filterd to set. * * @return void */ public function set_filters( array $filters ) { foreach ( $filters as $key => $value ) { // Nullable filters are not needed so we skip them. if ( null !== $value ) { // Make sure that setter is called for this specific key to preform validations and other things in setter function. if ( method_exists( $this, 'set_' . $key ) ) { $this->{'set_' . $key}( $value ); } else { $this->set_param( $key, $value ); } } } } /** * Set page. * * @param int $page Page to set. * * @return void */ public function set_page( int $page ) { $this->set_param( 'page', $page ); } /** * Set page size. * * @param int $page_size Page size to set. * * @return void */ public function set_page_size( int $page_size ) { $this->set_param( 'pagesize', $page_size ); } /** * Set sort by. * * @param string $sort Sort value. * * @return void */ public function set_sort_by( string $sort ) { $this->set_param( 'sort', $sort ); } /** * Set sort direction. * * @param string $direction Sort direction. * * @return void */ public function set_sort_direction( string $direction ) { $this->set_param( 'direction', $direction ); } } includes/core/server/request/class-list-documents.md 0000777 00000003170 15251156605 0016715 0 ustar 00 # `List_Documents` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\List_Documents` class is used to construct the request for listing documents. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |---------------|-------------------------------------------|:---------:|:--------:|:-------------:| | `match` | `set_match( string $match )` | - | - | - | | `page` | `set_page( int $page )` | Yes | - | - | | `pagesize` | `set_page_size( int $page_size )` | Yes | - | `25` | | `sort` | `set_sort_by( string $sort )` | Yes | - | `'created'` | | `direction` | `set_sort_direction( string $direction )` | Yes | - | `'desc'` | | `type_is` | `set_type_is( string $type_is )` | - | - | - | | `type_is_not` | `set_type_is_not( string $type_is_not )` | - | - | - | ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_list_documents_request` - Arguments: `$request` ## Example: ```php $request = List_Documents::create(); $request->set_match( $match ); $request->set_page( $page ); $request->set_page_size( $page_size ); $request->set_sort_by( $sort ); $request->set_sort_direction( $direction ); $request->set_type_is( $type_is ); $request->set_type_is_not( $type_is_not ); $request->send(); ``` includes/core/server/request/trait-level3.php 0000777 00000001663 15251156605 0015347 0 ustar 00 <?php /** * Trait file for WCPay\Core\Server\Request\Level3. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; /** * Tait for fixing intentions' level 3 data. */ trait Level3 { /** * Fixes level 3 data data inconsistencies. * * @param array $data The available level 3 data. * @return array The fixed level 3 data. */ private function fix_level3_data( array $data ) { // If level3 data doesn't contain any items, add a zero priced fee to meet Stripe's requirement. if ( ! isset( $data['line_items'] ) || ! is_array( $data['line_items'] ) || 0 === count( $data['line_items'] ) ) { $data['line_items'] = [ [ 'discount_amount' => 0, 'product_code' => 'empty-order', 'product_description' => 'The order is empty', 'quantity' => 1, 'tax_amount' => 0, 'unit_cost' => 0, ], ]; } return $data; } } includes/core/server/request/class-create-setup-intention.php 0000777 00000003744 15251156605 0020547 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request\Create_Setup_Intention. * * @package WooCommerce Payments */ namespace WCPay\Core\Server\Request; use WC_Payments; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request; use WC_Payments_API_Client; /** * Request class for creating setup intents. */ class Create_Setup_Intention extends Request { use Intention; use Level3; const IMMUTABLE_PARAMS = [ 'customer', 'confirm' ]; const REQUIRED_PARAMS = [ 'customer', 'payment_method_types' ]; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * @var string */ protected $hook = 'wcpay_create_setup_intention_request'; const DEFAULT_PARAMS = [ 'confirm' => 'false', ]; /** * Returns the request's API. * * @return string */ public function get_api(): string { return WC_Payments_API_Client::SETUP_INTENTS_API; } /** * Returns the request's HTTP method. */ public function get_method(): string { return 'POST'; } /** * Payment methods type setter. * * @param array $payment_methods List of payment methods. * * @return void */ public function set_payment_method_types( array $payment_methods ) { $this->set_param( 'payment_method_types', $payment_methods ); } /** * Customer setter. * * @param string $customer_id ID of the customer making the payment. * @return void * * @throws Invalid_Request_Parameter_Exception */ public function set_customer( string $customer_id ) { $this->validate_stripe_id( $customer_id, 'cus' ); $this->set_param( 'customer', $customer_id ); } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { return $this->api_client->deserialize_setup_intention_object_from_array( $response ); } } includes/core/server/request/class-list-fraud-outcome-transactions.md 0000777 00000003734 15251156605 0022202 0 ustar 00 # `List_Fraud_Outcome_Transactions` request class [ℹ️ This document is a part of __WooCommerce Payments Server Requests__](../README.md) ## Description The `WCPay\Core\Server\Request\List_Fraud_Outcome_Transactions` class is used to construct the request for listing fraud outcome transactions. ## Parameters | Parameter | Setter | Immutable | Required | Default value | |----------------------------|------------------------------------------------------------|:---------:|:--------:|:-------------:| | `status` | `set_status( string $status )` | Yes | - | - | | `page` | `set_page( int $page )` | Yes | - | - | | `pagesize` | `set_page_size( int $page_size )` | Yes | - | `25` | | `search` | `set_search( array $search )` | - | - | - | | `search_term` | `set_search_term( string $search_term )` | Yes | - | `'desc'` | | `sort` | `set_sort_by( string $sort )` | Yes | - | `'created'` | | `direction` | `set_sort_direction( string $direction )` | Yes | - | `'desc'` | ## Filter When using this request, provide the following filter and arguments: - Name: `wcpay_list_fraud_outcome_transactions_request` - Arguments: `$request` ## Example: ```php $request = List_Fraud_Outcome_Transactions::create(); $request->set_status( $status ); $request->set_page( $page ); $request->set_page_size( $page_size ); $request->set_search( $search ); $request->set_search_term( $search_term ); $request->set_sort_by( $sort ); $request->set_sort_direction( $direction ); $request->send(); ``` includes/core/server/CONTRIBUTING.md 0000777 00000023151 15251156605 0013060 0 ustar 00 # Creating new request classes for WooCommerce Payments 1. [Creating new requests](#creating-new-requests) 1. [Basic methods](#basic-methods) 1. [Identifiers](#identifiers) 1. [Setters](#setters) 1. [Parameter definitions](#parameter-definitions) 1. [Immutable parameters](#immutable-parameters) 1. [Validators](#validators) 1. [Extending requests during runtime](#extending-requests-during-runtime) 1. [Finding the definition](#1-look-for-the-definition) 2. [Creating a new extended class](#2-extend-the-class) 3. [Using the extended class](#3-replacing-the-class) 1. [Testing](#testing) ## Creating new requests This is the anatomy of a request-specific class: ```php namespace WCPay\Core\Server\Request; use WCPay\Core\Server\Request; class Update_Item extends Request { const DEFAULT_PARAMS = [ 'name' => '', 'origin' => 'uknown', ]; const REQUIRED_PARAMS = [ 'name', ]; const IMMUTABLE_PARAMS = [ 'name', ]; protected function set_id( string $id ) { $this->validate_stripe_id( $id ); $this->id = $id; } public function get_api(): string { return WC_Payments_API_Client::ITEMS_API . '/' . $this->id; } public function get_method(): string { return \Requests::POST; } public function set_name( string $name ) { $this->set_param( 'name', $name ); } } ``` ### Basic methods Each request class should define the otherwise abstract methods: - `get_api()` prepares the URL for the request. It's preferred to use one of the class constants of `WC_Payments_API_Client`, optionally followed by any other parts of the URL (ex. `$this->id . '/capture'`). - `get_method()` returns the HTTP method, preferably from the global `Requests` class. ### Identifiers Requests, which require an ID must define the `set_id` method, which validates the ID, and stores it within `$this->id`. Calling `Request::create( $id )` will automatically pass the identifier to the `set_id` method. ### Setters A setter should be provided for each parameter of the request. Setters: - Provide a way to **set a certain property**. The setter does not necessarily need to match the name of the property. - **Provide typehints** by having the correct type and DocBlocks to allow IDEs to do their job. - **Validate** the paramets. This can be done through custom functionality, or by using some of the [built-in validators, listed below](#validators). Once the value is available and validated, it should be stored through `$this->set_param( $name, $value )`. By using the functionality of the main `Request` class for this, there is no need for additional definition of parameters, and they can safely be stored without allowing them to be modified later (unless needed). ### Parameter definitions While none of those are required, parameter names can be included in a few special array constants: - `DEFAULT_PARAMS` allow pre-defining values to specific parameters. This is also useful for parameters, which do not have a setter, but need to be included in the request, ex: The `confirm` paramter of `Create_and_Confirm_Intent` is always present by definition, but does not have a setter. - `REQUIRED_PARAMS` should include all parameters, which must be present for the request to function. - `IMMUTABLE_PARAMS` is a bit more special. > 💡 All constants above are accummulated starting with the child class, and then the parent. > Extended requests can add, but not remove parameters. #### Immutable parameters Those parameters can be set in the main piece of code where the request is created and prepared, but cannot be modified after `$request->send()` is called. At this point all attempts to change the value of an immutable parameter, either through its main setter, or through an extended request will cause an exception. Example: ```php class My_Request { const IMMUTABLE_PARAMS = [ 'name', ]; public function set_name( $name ) { $this->set_param( 'name', $name ); } } add_filter( 'wcpay_my_request', function ( $request ) { $request->set_name( 'Hans' ); // <-- this will throw an `Immutable_Parameter_Exception` } ); $request = My_Request::create(); $request->set_name( 'John' ); $request->assign_hook( 'wcpay_my_request' ); $request->send(); ``` ### Validators Validators are stored in the abstract `Request` class, and they are used to validate arguments passed to setter methods. The setter method needs to call one (or many) validation method to use this functionality. - `validate_stripe_id( $id, $prefixes = null )` is used to validate Stripe IDs. Provide the ID, and optionally either a single prefix, on array of prefixes. This method can be used for all IDs, which generally follow the format `type_HASH`. - `validate_is_larger_than( $value_to_validate, $value_to_compare )` - `validate_currency_code( string $currency_code )` - `validate_date( string $date, string $format = 'Y-m-d H:i:s' )` All of those validators would throw an `Invalid_Request_Parameter_Exception` exception if the value is not in the correct format. Example: ```php /** * Sets the deposit ID. * * @param string $deposit_id The deposit ID, including. the `po_` prefix. * @throws Invalid_Request_Parameter_Exception When the provided ID is not valid. */ public function set_deposit( string $deposit_id ) { $this->validate_stripe_id( $deposit_id, 'po' ); $this->set_param( 'deposit', $deposit_id ); } ``` ## Extending requests during runtime Every request class can be extended, and replaced using filters. The important addition with requests is the static `extend()` method, which can be used on child classes to extend the parent request __object__ once it already exists. ### 1. Look for the definition Request classes can be extended as any other PHP class. Let's use the existing `Create_and_Confirm_Intention` and `WooPay_Create_and_Confirm_Intention` requests as an example. Here is how `Create_and_Confirm_Intention` is used in the gateway: ```php $request = Create_And_Confirm_Intention::create(); $request->set_hook_args( $payment_information ); // Call all necessary setters... $intent = $request->send(); ``` ### 2. Extend the class It can be easily extended: ```php class WooPay_Create_and_Confirm_Intention extends Create_and_Confirm_Intention { public function set_is_platform_payment_method() { $this->set_param( 'is_platform_payment_method', true ); } } ``` ### 3. Replacing the class The important part is how the new class is used. Instead of replacing `Create_and_Confirm_Intention` where it is used, please use the provided filter (`wcpay_create_intention_request` in this case) instead: ```php function replace_request( Create_and_Confirm_Intention $base_request, Payment_Information $payment_information ) { $request = WooPay_Create_and_Confirm_Intention::extend( $base_request ); $request->set_is_platform_payment_method(); return $request; } add_filter( 'wcpay_create_intention_request', 'replace_request', 10, 2 ); ``` Notice how `WooPay_Create_and_Confirm_Intention::extend()` is called here, and the provided argument is an instance of `Create_and_Confirm_Intention`. This mechanism copies the parameters of the existing request into the new one, but keeps them protected. #### Immutable parameters Even though the request is now an instance of another class, immutable parameters cannot be changed: ```php function replace_request( Create_and_Confirm_Intention $base_request, Payment_Information $payment_information ) { $request = WooPay_Create_and_Confirm_Intention::extend( $base_request ); $request->set_amount( 300 ); // <-- this will throw an exception. return $request; } ``` ## Testing To mock request classes, the recommended way is to use the built-in `mock_wcpay_request` function of `WCPAY_UnitTestCase`. ```php function mock_wcpay_request( // The request class you want to mock. string $request_class, // How many of the same API requests towards the server are expected to be executed. The default number is set to 1 and in the most cases you won't need to update this value // Could be `0` to simulate `->never()`. // If you have multiple execution of same request class (like retry mechanism or reusing the same request class instance), update this variable and match it with a number of executed API calls towards server. int $total_api_calls = 1, // ID of the item, which should be updated/retrieved, only needed for certain classes. string $request_class_constructor_id = null, // A response to be returned from the API. mixed $response = null ) : Request; ``` This will expect the request to be called `$total_api_calls` number of times. In addition, with this approach you can only mock the necessary request parameters. Here are some examples: __Standard mock__ ```php $mock_response = [ // add mock response here... ]; $request = $this->mock_wcpay_request( List_Documents::class, 1, null, $mock_response ); $request ->expects( $this->once() ) ->method( 'set_type_is' ) ->with( 'bill' ); ``` __A request, which should not be executed__ ```php $this->mock_wcpay_request( Create_Intention::class, 0 ); ``` __Throw an exception__ ```php $request = $this->mock_wcpay_request( Create_Intention::class );; $request ->expects( $this->once() ) ->method( 'format_response' ) ->will( $this->throwException( new API_Exception( /* ... */ ) ) ); ``` __Requests with custom responses__ Some request classes will try to parse and format the response they receive, in which case mocking it through the `$response_parameter` of `mock_wcpay_request` will not work. In those cases, you need to mock the `format_response` method: ```php $request = $this->mock_wcpay_request( Create_Intention::class );; $request ->expects( $this->once() ) ->method( 'format_response' ) ->willReturn( $mock_intention ); ``` includes/core/server/class-response.php 0000777 00000003467 15251156605 0014311 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Response. * * @package WooCommerce Payments */ namespace WCPay\Core\Server; use ArrayAccess; use WCPay\Core\Exceptions\Server\Response\Server_Response_Exception; /** * Represents responses from the WCPay server. */ class Response implements ArrayAccess { /** * Holds the data of the response. * * @var array */ protected $data; /** * Constructs the class. * * @param array $data The data for the response. */ public function __construct( array $data ) { $this->data = $data; } /** * Checks if a key exists. * * @param mixed $offset The key to check. * @return bool */ public function offsetExists( $offset ): bool { return isset( $this->data[ $offset ] ); } /** * Retrieves the value with a certain key. * * @param mixed $offset The key to retrieve. * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet( $offset ) { return $this->data[ $offset ]; } /** * Attempts to set a value in the response. * * @param mixed $offset The key of the value. * @param mixed $value The value. * @throws Server_Response_Exception It is not possible. */ public function offsetSet( $offset, $value ): void { throw new Server_Response_Exception( 'Server responses cannot be mutated.', 'wcpay_core_server_response_malformed' ); } /** * Removes a value from the response. * * @param mixed $offset The offset to remove. * @throws Server_Response_Exception It is not possible. */ public function offsetUnset( $offset ): void { throw new Server_Response_Exception( 'Server responses cannot be mutated.', 'wcpay_core_server_response_malformed' ); } /** * Return data as an array. * * @return array */ public function to_array() { return $this->data; } } includes/core/server/class-request.php 0000777 00000065150 15251156605 0014140 0 ustar 00 <?php /** * Class file for WCPay\Core\Server\Request. * * @package WooCommerce Payments */ namespace WCPay\Core\Server; use DateTime; use WC_Payments; use WC_Payments_Http_Interface; use WC_Payments_API_Client; use WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception; use WCPay\Core\Exceptions\Server\Request\Immutable_Parameter_Exception; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Core\Server\Request\Get_Request; use WCPay\Exceptions\API_Exception; use WP_Error; /** * Base for requests to the WCPay server. */ abstract class Request { /** * Contains a set of params, which the class considers immutable by others. * * Overwrite this in your class for individual properties. * * @var array<array-key, mixed> */ const IMMUTABLE_PARAMS = []; /** * Indicates which parameters are required (keys only). * * @var array<array-key, mixed> */ const REQUIRED_PARAMS = []; /** * Contains default values for parameters, which are not set automatically. * * @var array<array-key, mixed> */ const DEFAULT_PARAMS = []; /** * Holds the parameters of the request. * * @var array<array-key, mixed> */ private $params = []; /** * True when `->apply_filters()` is called to protect read-only props. * * In protected mode, if somebody tries to change an immutable param, * as declared in `IMMUTABLE_PARAMS`, an exception will be thrown. * * This way important params can be safe from modifications by extensions. * * @var bool */ private $protected_mode = false; /** * Stores the base class when `->apply_filters` is called. * This class will be checked when `::extend` is called. * * @var string */ private $base_class; /** * Holds the API client of WCPay. * * @var WC_Payments_API_Client */ protected $api_client; /** * Holds the HTTP interface of WCPay. * * @var WC_Payments_Http_Interface */ protected $http_interface; /** * Holds the ID of an item, which is included in the request URL. * * @var mixed (int|string) */ protected $id; /** * Specifies the WordPress hook name that will be triggered upon calling the send() method. * * Subclasses should define this property with a non-empty hook name, e.g.: * protected $hook = 'wcpay_my_request'; * * For dynamic hooks, use assign_hook() before sending the request. * * @var string */ protected $hook = ''; /** * Used to set the arguments for the request class WordPress hook. Make sure to add hook name first. * * @var array */ protected $hook_args = []; /** * Creates a new request, loading dependencies in there. * * @param mixed $id The identifier for various update/get/delete requests. * * @indexof $this->routeList * * @return static */ /** * Route list. * * @var string[] */ private $route_list = [ WC_Payments_API_Client::ACCOUNTS_API => 'accounts', WC_Payments_API_Client::CAPABILITIES_API => 'accounts/capabilities', WC_Payments_API_Client::WOOPAY_ACCOUNTS_API => 'accounts/platform_checkout', WC_Payments_API_Client::WOOPAY_COMPATIBILITY_API => 'woopay/compatibility', WC_Payments_API_Client::DOMAIN_REGISTRATION_API => 'payment_method_domains', WC_Payments_API_Client::CHARGES_API => 'charges', WC_Payments_API_Client::CONN_TOKENS_API => 'terminal/connection_tokens', WC_Payments_API_Client::TERMINAL_LOCATIONS_API => 'terminal/locations', WC_Payments_API_Client::CUSTOMERS_API => 'customers', WC_Payments_API_Client::CURRENCY_API => 'currency', WC_Payments_API_Client::INTENTIONS_API => 'intentions', WC_Payments_API_Client::REFUNDS_API => 'refunds', WC_Payments_API_Client::DEPOSITS_API => 'deposits', WC_Payments_API_Client::TRANSACTIONS_API => 'transactions', WC_Payments_API_Client::DISPUTES_API => 'disputes', WC_Payments_API_Client::FILES_API => 'files', WC_Payments_API_Client::ONBOARDING_API => 'onboarding', WC_Payments_API_Client::TIMELINE_API => 'timeline', WC_Payments_API_Client::PAYMENT_METHODS_API => 'payment_methods', WC_Payments_API_Client::SETUP_INTENTS_API => 'setup_intents', WC_Payments_API_Client::TRACKING_API => 'tracking', WC_Payments_API_Client::PAYMENT_PROCESS_CONFIG_API => 'payment_process_config', WC_Payments_API_Client::PRODUCTS_API => 'products', WC_Payments_API_Client::PRICES_API => 'products/prices', WC_Payments_API_Client::INVOICES_API => 'invoices', WC_Payments_API_Client::SUBSCRIPTIONS_API => 'subscriptions', WC_Payments_API_Client::SUBSCRIPTION_ITEMS_API => 'subscriptions/items', WC_Payments_API_Client::READERS_CHARGE_SUMMARY => 'reader-charges/summary', WC_Payments_API_Client::TERMINAL_READERS_API => 'terminal/readers', WC_Payments_API_Client::MINIMUM_RECURRING_AMOUNT_API => 'subscriptions/minimum_amount', WC_Payments_API_Client::CAPITAL_API => 'capital', WC_Payments_API_Client::PROMOTIONS_API => 'payment_method_promotions', WC_Payments_API_Client::WEBHOOK_FETCH_API => 'webhook/failed_events', WC_Payments_API_Client::DOCUMENTS_API => 'documents', WC_Payments_API_Client::VAT_API => 'vat', WC_Payments_API_Client::LINKS_API => 'links', WC_Payments_API_Client::AUTHORIZATIONS_API => 'authorizations', WC_Payments_API_Client::FRAUD_OUTCOMES_API => 'fraud_outcomes', WC_Payments_API_Client::FRAUD_RULESET_API => 'fraud_ruleset', WC_Payments_API_Client::COMPATIBILITY_API => 'compatibility', ]; /** * Creates a new request, loading dependencies in there. * * @param mixed $id The identifier for various update/get/delete requests. * * @return static */ public static function create( $id = null ) { return WC_Payments::create_request( static::class, $id ); } /** * GET Request wrapped for easier request creation. * * @param string $api Api method. * @param mixed $id An optional ID for the item that will be updated/retrieved/deleted. * * @return Request|Get_Request * * @throws Invalid_Request_Parameter_Exception|\Exception */ public static function get( string $api, $id = null ) { /** * Request variable. * * @var Get_Request $request */ $request = WC_Payments::create_request( Get_Request::class, $id ); $request->set_api( $api ); return $request; } /** * Prevents the class from being constructed directly. * * @param WC_Payments_API_Client $api_client The API client to use to send requests. * @param WC_Payments_Http_Interface $http_interface The HTTP interface for the server. * @param mixed $id An optional ID for the item that will be updated/retrieved/deleted. * * @throws Invalid_Request_Parameter_Exception */ public function __construct( WC_Payments_API_Client $api_client, WC_Payments_Http_Interface $http_interface, $id = null ) { $this->api_client = $api_client; $this->http_interface = $http_interface; $this->set_request_route_id_parameter( $id ); } /** * Returns the needed API. * * @return string Check WCPay\Core\Server\APIs. */ abstract public function get_api(): string; /** * Returns the method of the request. * * @return string See the constants in WordPress's `Requests` class. */ abstract public function get_method(): string; /** * This is a legacy method, and is the same throughout the codebase. * Might be worth removing while refactoring to use the Core\Server API. * * @return bool */ public function is_site_specific(): bool { return true; } /** * If true, the request will be signed with the user token rather than blog token. Defaults to false. * * @return bool */ public function should_use_user_token(): bool { return false; } /** * Indicates if the raw response should be returned. * * @return bool */ public function should_return_raw_response(): bool { return false; } /** * Returns all of the parameters for the request. * * @return array * @throws Invalid_Request_Parameter_Exception If the request has not been initialized yet. */ final public function get_params() { $defaults = static::get_default_params(); $params = array_merge( $defaults, $this->params ); $missing_params = []; foreach ( static::get_required_params() as $name ) { if ( ! isset( $params[ $name ] ) ) { $missing_params[] = $name; } } if ( ! empty( $missing_params ) ) { throw new Invalid_Request_Parameter_Exception( esc_html( sprintf( 'Trying to access the parameters of a request which is not (fully) initialized yet. Missing parameter(s) for %s: %s', get_class( $this ), implode( ', ', $missing_params ) ) ), 'wcpay_core_invalid_request_parameter_missing_parameters' ); } foreach ( $params as $key => $value ) { if ( true === $value ) { // The WCPay server requires the string 'true'. $params[ $key ] = 'true'; } } return $params; } /** * Get request param by key. * * @param string $key Key to get. * * @return mixed * @throws Invalid_Request_Parameter_Exception */ final public function get_param( $key ) { if ( array_key_exists( $key, $this->params ) ) { return $this->params[ $key ]; } throw new Invalid_Request_Parameter_Exception( esc_html( sprintf( 'The passed key %s does not exist in Request class', $key ) ), 'wcpay_core_invalid_request_parameter_uninitialized_param' ); } /** * Allows the request to be modified, and then sends it. * * @return mixed Either the response array, or the correct object. * * @throws Extend_Request_Exception * @throws Immutable_Parameter_Exception * @throws Invalid_Request_Parameter_Exception */ final public function send() { $hook = $this->get_hook(); // Validate that a hook name is defined. Using an empty hook name can cause // collisions with other plugins that accidentally register filters to empty strings, // leading to unexpected behavior or fatal errors on PHP 8.0+. if ( '' === $hook ) { throw new Invalid_Request_Parameter_Exception( sprintf( 'Request class %s must define a non-empty hook name. Set the $hook property or call assign_hook() before sending.', static::class ), 'wcpay_core_invalid_request_parameter_missing_hook' ); } return $this->format_response( $this->api_client->send_request( $this->apply_filters( $hook, ...$this->hook_args ) ) ); } /** * This is mimic of send method, but where API exception is handled. * The reason behind this is that sometimes API request can fail for valid reasons and instead of handling this exception on every request, you could use this function. * * @return mixed Either the response array, or the correct object. * * @throws Extend_Request_Exception * @throws Immutable_Parameter_Exception * @throws Invalid_Request_Parameter_Exception */ final public function handle_rest_request() { try { $data = $this->send(); // Make sure to return array if $data is instance or has parent as a Response class. if ( is_a( $data, Response::class ) ) { return $data->to_array(); } // Return the data and let caller to parse it as it pleases. return $data; } catch ( API_Exception $e ) { return new WP_Error( $e->get_error_code(), $e->getMessage() ); } } /** * Formats the response from the server. * * @param mixed $response The response from `WC_Payments_API_Client::request`. * @return mixed Either the same response, or the correct object. */ public function format_response( $response ) { return new Response( $response ); } /** * Used to validate passed ID from the constructor. You can easily override it if you need custom id handling in your classes. * * @param mixed $id ID parameter. * * @return void * @throws Invalid_Request_Parameter_Exception */ protected function set_request_route_id_parameter( $id ) { if ( method_exists( $this, 'set_id' ) ) { if ( null !== $id ) { $this->set_id( $id ); } else { throw new Invalid_Request_Parameter_Exception( 'This request requires an item ID.', 'wcpay_core_invalid_request_parameter_missing_id' ); } } } /** * Returns the WordPress hook name for this request. * * This method is final to ensure consistent behavior with assign_hook(). * To define a hook, set the $hook property in your subclass or use assign_hook(). * * @return string The hook name. */ final public function get_hook(): string { return $this->hook; } /** * Assign the WordPress hook and the arguments specific to the previously assigned hook. * * @param string $hook WordPress hook name. */ public function assign_hook( string $hook ) { $this->hook = $hook; } /** * Set hook arguments. Used when hook is predefined in the request class, but you want to pass hook args. * * @param mixed ...$args Arguments for the hook. * @return void */ public function set_hook_args( ...$args ) { $this->hook_args = $args; } /** * Stores a parameter within the internal props. * * Use this method within child classes in order to allow * those properties to be protected by overwriting. * * @param string $key The name of the parameter. * @param mixed $value And the value to set. */ final protected function set_param( string $key, $value ) { if ( $this->protected_mode && in_array( $key, static::get_immutable_params(), true ) ) { $this->throw_immutable_exception( $key ); } $this->params[ $key ] = $value; } /** * Unsets an existing parameter if it was set before. * * @param string $key The key of the parameter. */ final protected function unset_param( string $key ) { if ( $this->protected_mode && in_array( $key, static::get_immutable_params(), true ) ) { $this->throw_immutable_exception( $key ); } if ( isset( $this->params[ $key ] ) ) { unset( $this->params[ $key ] ); } } /** * Replaces all internal parameters of the class. * Only accessible from methods of this class, used for the `extend` method. * * @param array $params The new parameters to use. */ private function set_params( $params ) { $this->params = $params; } /** * Creates a new instance of the called class with the same props * as an existing request, which must be of a parent class. * * This method is only available within `apply_filters()`. * * @param Request $base_request The request to extend. * @return static An instance of the class. * @throws Extend_Request_Exception In case this is not a subclass of the base request. */ final public static function extend( Request $base_request ) { $current_class = static::class; $base_request->validate_extended_class( $current_class, $base_request->base_class ?? get_class( $base_request ) ); if ( ! $base_request->protected_mode ) { throw new Extend_Request_Exception( esc_html( get_class( $base_request ) . ' can only be extended within its ->apply_filters() method.' ), 'wcpay_core_extend_class_incorrectly' ); } $obj = new $current_class( $base_request->api_client, $base_request->http_interface, $base_request->id ?? null ); $obj->set_params( array_merge( static::DEFAULT_PARAMS, $base_request->params ) ); // Carry over the base class and protected mode into the child request. $obj->base_class = $base_request->base_class; $obj->protected_mode = true; return $obj; } /** * Allows the request to be altered/replaced through a filter. * * Call this method when the request has been completely prepared, * and is ready to be sent to the server. At this point functions, * which hook into the filter cannot alter the IMMUTABLE_PARAMS * of the request anymore. Instead they can either modify the other * mutable params, or extend the request. * * @param string $hook The filter to use. * @param mixed ...$args Other parameters for the hook. * @return static Either the same instance, or an object from a sub-class. * @throws Extend_Request_Exception In case a class does not exist. * @throws Immutable_Parameter_Exception In case an immutable propery is tried to change. * @throws Invalid_Request_Parameter_Exception In case an invalid property is passed. */ final public function apply_filters( $hook, ...$args ) { // Lock the class in order to prevent `set_param` for protected props. $this->protected_mode = true; $this->base_class = get_class( $this ); // Validate API route. $this->validate_api_route( $this->get_api() ); /** * Allows a request to be modified, extended or replaced. * * @param Request $request The request to modify. * @param mixed ...$args Other provided parameters for the hook. * @return Request Either the same request, or a sub-class. */ $replacement = apply_filters( $hook, $this, ...$args ); // Exit protected mode right after `apply_filters`. $this->protected_mode = false; $my_class = get_class( $this ); $new_class = get_class( $replacement ); if ( $new_class !== $my_class ) { $this->validate_extended_class( $new_class, $my_class ); } // NB: `array_diff` will only pick up updated props, not new ones. $difference = $this->array_diff( $this->params, $replacement->params ); if ( empty( $difference ) ) { // Nothing got overwritten, it's the same request, or one with only new props. return $replacement; } foreach ( static::get_immutable_params() as $param ) { if ( isset( $difference[ $param ] ) ) { $this->throw_immutable_exception( $param ); } } return $replacement; } /** * Throws an exception upon attempts to mutate an immutable parameter. * * @param string $param The name of the param. * @throws Immutable_Parameter_Exception An exception, which indicates which property is immutable. */ private function throw_immutable_exception( string $param ) { throw new Immutable_Parameter_Exception( esc_html( sprintf( 'The value of %s::%s is immutable and cannot be changed.', get_class( $this ), $param ) ), 'wcpay_core_immutable_parameter_changed' ); } /** * Returns an array with the names of params, which should not be modified. * * @return string[] The names of those params. */ public static function get_immutable_params() { return static::traverse_class_constants( 'IMMUTABLE_PARAMS', true ); } /** * Returns an array with the names of params, which are required. * * @return string[] The names of those params. */ public static function get_required_params() { return static::traverse_class_constants( 'REQUIRED_PARAMS', true ); } /** * Returns an array with the combined default params from all classes. */ public static function get_default_params() { return static::traverse_class_constants( 'DEFAULT_PARAMS' ); } /** * Combines array constants from a class's tree. * * @param string $constant_name The name of the constant to load. * @param bool $unique Whether to return unique items. Useful with numeric keys. * @return string[] The unique combined values from the arrays. */ public static function traverse_class_constants( string $constant_name, bool $unique = false ) { $keys = []; $class_name = static::class; do { $constant = "$class_name::$constant_name"; if ( defined( $constant ) ) { $keys = array_merge( constant( $constant ), $keys ); } $class_name = get_parent_class( $class_name ); } while ( $class_name ); if ( $unique ) { $keys = array_unique( $keys ); } return $keys; } /** * Generates the difference between two arrays. * * @param array $array1 The first array. * @param array $array2 The second array. * @return array The difference between the two arrays. */ private function array_diff( $array1, $array2 ) { $arr_to_json = function ( $item ) { return is_array( $item ) ? wp_json_encode( $item ) : $item; }; return array_diff_assoc( array_map( $arr_to_json, $array1 ), array_map( $arr_to_json, $array2 ) ); } /** * Validates Stripe identifiers. * * @param string $id The identifier to validate. * @param mixed|null $prefixes A prefix or an array of them (Optional). * @throws Invalid_Request_Parameter_Exception An exception if the format is not matched. * @return void */ protected function validate_stripe_id( $id, $prefixes = null ) { if ( empty( $id ) ) { throw new Invalid_Request_Parameter_Exception( esc_html__( 'Empty parameter is not allowed', 'woocommerce-payments' ), 'wcpay_core_invalid_request_parameter_stripe_id' ); } if ( is_null( $prefixes ) ) { $prefixes = '[a-z]+'; } else { if ( ! is_array( $prefixes ) ) { $prefixes = [ $prefixes ]; } $prefixes = '(' . implode( '|', array_map( 'preg_quote', $prefixes ) ) . ')'; } /** * IDs include a prefix (a few characters), and are up to 255 characters long. * * @see https://stripe.com/docs/upgrades#what-changes-does-stripe-consider-to-be-backwards-compatible */ $regex = "/^{$prefixes}_\w{1,250}$/"; if ( preg_match( $regex, $id ) ) { return; } throw new Invalid_Request_Parameter_Exception( esc_html( sprintf( // Translators: %s is a Stripe ID. __( '%s is not a valid Stripe identifier', 'woocommerce-payments' ), $id ) ), 'wcpay_core_invalid_request_parameter_stripe_id' ); } /** * Validate is number larger than passed compared number. * * @param float $value_to_validate Value to validate. * @param float $value_to_compare Value to compare. * @throws Invalid_Request_Parameter_Exception An exception if the format is not matched. * @return void */ protected function validate_is_larger_than( float $value_to_validate, float $value_to_compare ) { if ( $value_to_validate > $value_to_compare ) { return; } throw new Invalid_Request_Parameter_Exception( esc_html( sprintf( /* translators: %1$s and %2$s are both numbers */ __( 'Invalid number passed. Number %1$s needs to be larger than %2$s', 'woocommerce-payments' ), $value_to_validate, $value_to_compare ) ), 'wcpay_core_invalid_request_parameter_order' ); } /** * Currency code validator. * * @param string $currency_code Currency code. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function validate_currency_code( string $currency_code ) { $account_data = WC_Payments::get_account_service()->get_cached_account_data(); if ( isset( $account_data['customer_currencies']['supported'] ) && ! in_array( $currency_code, $account_data['customer_currencies']['supported'], true ) ) { throw new Invalid_Request_Parameter_Exception( esc_html( sprintf( // Translators: %s is a currency code. __( '%s is not a supported currency for payments.', 'woocommerce-payments' ), $currency_code ) ), 'wcpay_core_invalid_request_parameter_currency_not_available' ); } } /** * Extend class validator. * * @param mixed $child_class Child class. * @param string $parent_class Parent class. * * @return void * @throws Extend_Request_Exception */ public function validate_extended_class( $child_class, string $parent_class ) { if ( ! is_subclass_of( $child_class, $parent_class ) ) { throw new Extend_Request_Exception( esc_html( sprintf( 'Failed to extend request. %s is not a subclass of %s', is_string( $child_class ) ? $child_class : get_class( $child_class ), $parent_class ) ), 'wcpay_core_extend_class_not_subclass' ); } } /** * Validate date with given format. * * @param string $date Date to validate. * @param string $format Format to check. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function validate_date( string $date, string $format = 'Y-m-d H:i:s' ) { $d = DateTime::createFromFormat( $format, $date ); if ( ! ( $d && $d->format( $format ) === $date ) ) { throw new Invalid_Request_Parameter_Exception( esc_html( sprintf( // Translators: %1$s is a provided date string, %2$s is a date format. __( '%1$s is not a valid date for format %2$s.', 'woocommerce-payments' ), $date, $format ) ), 'wcpay_core_invalid_request_parameter_invalid_date' ); } } /** * Validate a redirect URL in the allowed_redirect_hosts filter. * * @param string $redirect_url The provided redirect URL. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function validate_redirect_url( string $redirect_url ) { $check_fallback_url = wp_generate_password( 12, false ); if ( hash_equals( $check_fallback_url, wp_validate_redirect( $redirect_url, $check_fallback_url ) ) ) { throw new Invalid_Request_Parameter_Exception( esc_html( sprintf( // Translators: %s is a currency code. __( '%1$s is not a valid redirect URL. Use a URL in the allowed_redirect_hosts filter.', 'woocommerce-payments' ), $redirect_url ) ), 'wcpay_core_invalid_request_parameter_invalid_redirect_url' ); } } /** * Validate if the username exists and is valid on the site. * * @param string $user_name Username to validate. * * @return void * @throws Invalid_Request_Parameter_Exception */ public function validate_user_name( string $user_name ) { $user = get_user_by( 'login', $user_name ); if ( false === $user ) { throw new Invalid_Request_Parameter_Exception( esc_html( sprintf( // Translators: %s is a provided username. __( '%s is not a valid username.', 'woocommerce-payments' ), $user_name ) ), 'wcpay_core_invalid_request_parameter_invalid_username' ); } } /** * Validates API endpoint or route. * * @param string $api_route API route to validate. * * @throws Invalid_Request_Parameter_Exception */ public function validate_api_route( string $api_route ) { $api_route = explode( '/', $api_route ); // In case if you have something after "/" like id or something similar. // Some routes have 2 URIs. In case route we want to validate have 2 (or more) URI,s lets validate that first. // There could be micro optimization to validate only array keys with two 'URI's but for now we can skip that part. if ( ( count( $api_route ) > 1 && array_key_exists( "$api_route[0]/$api_route[1]", $this->route_list ) ) || array_key_exists( $api_route[0], $this->route_list ) ) { return; } throw new Invalid_Request_Parameter_Exception( 'Invalid request api route', 'wcpay_core_invalid_request_parameter_api_route_not_defined' ); } } includes/core/server/README.md 0000777 00000017201 15251156605 0012105 0 ustar 00 # WooCommerce Payments Server Requests This document outlines the concepts and implementation of WooCommerce Payments server request classes. The purpose of request classes is to provide an easy and structured way to send API requests towards WooCommerce Payments Server, where type safety, validation and builder pattern is used to built requests towards the server. To make it easier for other parties to use it and extend it, every request class has its own filter that can be hooked on, and you could change the behavior or even set your own parameters while maintain all functionality from the original request class. 1. [Usage](#usage) 1. [Using `GET` and `DELETE` requests](#using-get-and-delete-requests) 1. [Using `POST` requests](#using-post-requests) 1. [Generic requests](#generic-requests) 1. [Providing and using filters](#providing-and-using-filters) 1. [Providing filters](#providing-filters) 1. [Using filters](#using-filters) 1. [Available requests](#available-requests) 1. [Responses](#responses) 1. [Creating new requests and extending existing ones](#creating-new-requests-and-extending-existing-ones) ## Usage In general, to use any specific request class to make a request, you need to: 1. Create the request. 2. Prepare the request by calling all necessary setters (mainly for `POST` requests). 3. Send the request. #### Using `GET` and `DELETE` requests Requests to retrieve or delete items should be performed similarly to this: ```php <?php use WCPay\Core\Server\Request\Get_Intention; $intention_id = $this->order_service->get_intent_id_for_order( $order ); $request = Get_Intention::create( $intention_id ); $request->assign_hook( 'my_get_intention_request', $order ); /** * Sends a request to retrieve an intention. * * @param WC_Order $order The order, which the intent is associated with. */ $intention = $request->send(); ``` Highlights from this example: 1. For `GET` requests the item identifier is a required parameter for the `::create()` method. 2. Additional parameters should be providable only through setters (see examples with `POST` requests below). 3. Whenever sending the request, it is required to provide a filter. #### Using `POST` requests Requests to create/update items should look similarly to this: ```php <?php use WCPay\Core\Server\Request\Create_Intention; $request = Create_Intention::create(); $request->set_amount( WC_Payments_Utils::prepare_amount( $amount, $order->get_currency() ) ); $request->set_currency( $order->get_currency() ); $request->set_payment_method( $payment_information->get_payment_method() ); $intention = $request->send(); ``` ### Generic requests [➡️ Generic Requests Documentation](request/class-generic.md) The `WCPay\Core\Server\Request\Generic` class is available to use during development without needing to create a new request class. ```php use WCPay\Core\Server\Request\Generic; $request = new Generic( 'payment_intentions', \Requests::POST, [ 'amount' => 300 ] ); $intention = $request->send(); ``` However, once the rest of the related development is finished, please create a new request class before merging. ## Providing and using filters Request classes are build from the ground up with the idea of being safely extendable. Every time a request is sent, it is mandatory to provide at least a filter, and potentially arguments for that filter, in order to allow others to modify it. Filters in request work similarly to generic WordPress filter, with the difference that they are provided through the request. This allows protected (immutable) parameters to remain protected, and slightly decreases the overhead of checking the returned request. ### Providing filters It is mandatory to provide a filter when sending each request. When doing so, please try to provide enough context for the filter, but do not provide parameters, which are already in the filter. > The `->send()` method of any request should be documented similarly to any other WordPress filter/action. If you are using the same hook name as another instance, please use the same parameters. Here is a **good example**: ```php /** * Sending a request to create and confirm a payment intention. * * @param WC_Order $order The order which the intention belongs to. * @param Payment_Information $payment_information Prepared payment information from the gateway. */ $request->assign_hook( 'wcpay_update_intention_request' ); $request->set_hook_args( $order, $payment_information ) $intention = $request->send(); ``` - This example includes everything needed in the `$order` and `$payment_information` parameters, and they are documented. - The hook name is prefixed with `wcpay_`. Here is a **bad example**: ```php $request->assign_hook('update_request', $intention_id ); $intention = $request->send(); ``` - This example uses `update_request`, which could appear in other hooks. - The relevant order and payment information objects are not present. - `$intention_id` is redundant, as it's already available though the request: `$request->get_id()`. - There is no PHPDoc comment for the request, leaving others guessing what types to expect. ### Using filters Based on the good example above, a request can be modified in the following way: ```php add_filter( 'wcpay_update_intention_request', 'my_update_intention_callback', 10, 3 ); function my_update_intention_callback( $request, WC_Order $order, \WCPay\Payment_Information $payment_information ) { $request->set_metadata( [] ); $request->set_amount( 300 ); // Do not try this, `amount` is protected, and cannot be modified through filters. return $request; } ``` Note that you need to increase the callback parameter count by 1 in order to receive both the request, and all additional context. ## Available requests Currently the WooCommerce Payments API client contains around 100 requests. The plan is to cover all of them in time, in the meantime here is a list of the existing requests: - Intentions - [Cancel_Intention](request/class-cancel-intention.md) - [Capture_Intention](request/class-capture-intention.md) - [Create_And_Confirm_Intention](request/class-create-and-confirm-intention.md) - [Create_And_Confirm_Setup_Intention](request/class-create-and-confirm-setup-intention.md) - [Create_Intention](request/class-create-intention.md) - [Create_Setup_Intention](request/class-create-setup-intention.md) - [Get_Intention](request/class-get-intention.md) - [Update_Intention](request/class-update-intention.md) - [Get_Charge](request/class-get-charge.md) - Paginated lists (mainly used for the REST API): - [List_Authorizations](request/class-list-authorizations.md) - [List_Deposits](request/class-list-deposits.md) - [List_Disputes](request/class-list-disputes.md) - [List_Documents](request/class-list-documents.md) - [List_Transactions](request/class-list-transactions.md) - [List_Fraud_Outcome_Transactions](request/class-list-fraud-outcome-transactions.md) - WooPay-specific - [WooPay_Create_Intent](request/class-woopay-create-intent.md) - [WooPay_Create_And_Confirm_Intention](request/class-woopay-create-and-confirm-intention.md) - [WooPay_Create_And_Confirm_Setup_Intention](request/class-woopay-create-and-confirm-setup-intention.md) ## Responses Please check the documentation of specific requests to see their particular response types and formats. Unless specified otherwise, most requests will return a `WCPay\Core\Server\Reponse` object, which can be treated as an array (implements `ArrayAccess`), representing the server response. Additionally to that, specific requests might have additional getters and validators. ## Creating new requests and extending existing ones Please refer to [contributor docs](CONTRIBUTING.md). includes/core/CONTRIBUTING.md 0000777 00000005557 15251156605 0011564 0 ustar 00 # Contributing to WooCommerce Payments Core The [main readme file](README.md) in this directory outlines the concept of WooCommerce Payments' core. Please familiarize yourself with it before proceeding with this document. ## Existing and additional core functionality The initial (and so far current) contents of this directory, as described in README.md is the outcome of the initial phase of the WooCommerce Payments as a Platform project. However, in addition to further phases of the project, further contributions to the plugin should also strive to follow the same principles. ### Services There are a few possible paths when it comes to services: 1. __Create a facade for an existing service:__ Create a new service class within `core/service`, which simply facades the [existing service](service/customer-service.md). Doing so will allow us to modify the facade in the future, keeping existing methods with the same parameters as existing ones. This is what was done with the [customer service](service/customer-service.md), and is the recommended way if a certain feature requires access to an existing service quickly. 2. __Move an existing service to the core directory:__ This should be done with consideration how the service could change in the future, and whether it is core to the gateway. If it more suitable to an extension (ex. [Multi-Currency](https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/)), or a consumer (ex. [WooPay](https://woocommerce.com/documentation/products/woopay/)), it likely needs to be somewhere else. 3. When __creating a new service__, similarly to moving existing ones here, please consider whether the service belongs to core. If it does, do it with care, as services should be reliable and resilient. 🔗 Further information about services in core is available [within the services directory](services/README.md). ### Communication with the server The bulk of the communication between the client (plugin) and server is currently done through the `WC_Payments_API_Client` class. The foundation for request and response classes has already been laid down. However, not all requests have been converted to classes yet, and there is only a generic response class available for the moment. Whenever creating new requests, please consider using request classes for them. [🔗 Read more about server requests](server/README.md) [🔗 Guide for creating server requests classes](server/CONTRIBUTING.md) ## Using core functionality While working on WooCommerce Payments, please consider the concept of core vs non-core functionality. A lot of projects could be considered a part of WooCommerce Payments, WooPay being a good example. It's a part of WooCommerce Payments, but is not a part of core. When using core APIs within isolated code, it will allow new developers to understand fewer products, and decrease the likelyhoold of side-effects. includes/core/service/class-wc-payments-customer-service-api.php 0000777 00000014163 15251156605 0021113 0 ustar 00 <?php /** * Class file for WCPay\Core\WC_Payments_Customer_Service_API. * * @package WooCommerce Payments */ namespace WCPay\Core; use WCPay\Exceptions\API_Exception; use WC_Payments_Customer_Service; use WP_User; use WC_Customer; use WC_Order; use WC_Payments_Token_Service; defined( 'ABSPATH' ) || exit; /** * Class handling any customer functionality */ class WC_Payments_Customer_Service_API { /** * Internal Customer_Service instance to invoke. * * @var WC_Payments_Customer_Service */ private $customer_service; /** * Internal Token_Service instance to invoke. * * @var WC_Payments_Token_Service */ private $token_service; /** * Class constructor * * @param WC_Payments_Customer_Service $customer_service Customer Service instance. * @param WC_Payments_Token_Service $token_service Token Service instance. */ public function __construct( WC_Payments_Customer_Service $customer_service, WC_Payments_Token_Service $token_service ) { $this->customer_service = $customer_service; $this->token_service = $token_service; } /** * Get WCPay customer ID for the given WordPress user ID * * @param int $user_id The user ID to look for a customer ID with. * * @return string|null WCPay customer ID or null if not found. */ public function get_customer_id_by_user_id( $user_id ) { return $this->customer_service->get_customer_id_by_user_id( $user_id ); } /** * Create a customer and associate it with a WordPress user. * * @param WP_User $user User to create a customer for. * @param array $customer_data Customer data. * * @return string The created customer's ID * * @throws API_Exception Error creating customer. */ public function create_customer_for_user( WP_User $user, array $customer_data ): string { return $this->customer_service->create_customer_for_user( $user, $customer_data ); } /** * Update the customer details held on the WCPay server associated with the given WordPress user. * * @param string $customer_id WCPay customer ID. * @param WP_User $user WordPress user. * @param array $customer_data Customer data. * * @return string The updated customer's ID. Can be different to the ID parameter if the customer was re-created. * * @throws API_Exception Error updating the customer. */ public function update_customer_for_user( string $customer_id, WP_User $user, array $customer_data ): string { return $this->customer_service->update_customer_for_user( $customer_id, $user, $customer_data ); } /** * Sets a payment method as default for a customer. * * @param string $customer_id The customer ID. * @param string $payment_method_id The payment method ID. */ public function set_default_payment_method_for_customer( $customer_id, $payment_method_id ) { return $this->customer_service->set_default_payment_method_for_customer( $customer_id, $payment_method_id ); } /** * Gets all payment methods for a customer. * * @param string $customer_id The customer ID. * @param string $type Type of payment methods to fetch. * * @throws API_Exception We only handle 'resource_missing' code types and rethrow anything else. */ public function get_payment_methods_for_customer( $customer_id, $type = 'card' ) { return $this->customer_service->get_payment_methods_for_customer( $customer_id, $type ); } /** * Updates a customer payment method. * * @param string $payment_method_id The payment method ID. * @param WC_Order $order Order to be used on the update. */ public function update_payment_method_with_billing_details_from_order( $payment_method_id, $order ) { return $this->customer_service->update_payment_method_with_billing_details_from_order( $payment_method_id, $order ); } /** * Clear payment methods cache for a user. * * @param int $user_id WC user ID. */ public function clear_cached_payment_methods_for_user( $user_id ) { return $this->token_service->clear_cached_payment_methods_for_user( $user_id ); } /** * Given a WC_Order or WC_Customer, returns an array representing a Stripe customer object. * At least one parameter has to not be null. * * @param WC_Order $wc_order The Woo order to parse. * @param WC_Customer $wc_customer The Woo customer to parse. * * @return array Customer data. */ public static function map_customer_data( WC_Order $wc_order, WC_Customer $wc_customer ): array { return WC_Payments_Customer_Service::map_customer_data( $wc_order, $wc_customer ); } /** * Delete all saved payment methods that are stored inside database cache driver. * * @return void */ public function delete_cached_payment_methods() { $this->token_service->clear_all_cached_payment_methods(); } /** * Get the WCPay customer ID associated with an order, or create one if none found. * * @param WC_Order $order WC Order object. * * @return string|null WCPay customer ID. * @throws API_Exception If there's an error creating customer. */ public function get_customer_id_for_order( $order ) { return $this->customer_service->get_customer_id_for_order( $order ); } /** * Updates the given user with the given WooPayments * customer ID. * * @param int $user_id The WordPress user ID. * @param string $customer_id The WooPayments customer ID. */ public function update_user_customer_id( int $user_id, string $customer_id ) { $this->customer_service->update_user_customer_id( $user_id, $customer_id ); } /** * Adds the WooComerce Payments customer ID found in the user session * to the WordPress user as metadata. * * @param int $user_id The WordPress user ID. */ public function add_customer_id_to_user( $user_id ) { $this->customer_service->add_customer_id_to_user( $user_id ); } /** * Prepares customer data to be used on 'Pay for Order' or 'Add Payment Method' pages. * Customer data is retrieved from order when on Pay for Order. * Customer data is retrieved from customer when on 'Add Payment Method'. * * @return array|null An array with customer data or nothing. */ public function get_prepared_customer_data() { return $this->customer_service->get_prepared_customer_data(); } } includes/core/service/customer-service.md 0000777 00000011653 15251156605 0014606 0 ustar 00 # WooCommerce Payments Core Customer Service The customer service is available through the `WC_Payments_Customer_Service_API` class, available through `WC_Payments::get_customer_service_api()`: Example: ```php $payment_methods = WC_Payments::get_customer_service_api()->get_payment_methods_for_customer( 'cus_XXX', 'card' ); ``` ## Available methods - [`get_customer_id_by_user_id()`](#get_customer_id_by_user_id) - [`create_customer_for_user()`](#create_customer_for_user) - [`update_customer_for_user()`](#update_customer_for_user) - [`set_default_payment_method_for_customer()`](#set_default_payment_method_for_customer) - [`get_payment_methods_for_customer()`](#get_payment_methods_for_customer) - [`update_payment_method_with_billing_details_from_order()`](#update_payment_method_with_billing_details_from_order) - [`clear_cached_payment_methods_for_user()`](#clear_cached_payment_methods_for_user) - [`map_customer_data()`](#map_customer_data) - [`delete_cached_payment_methods()`](#delete_cached_payment_methods) - [`get_customer_id_for_order()`](#get_customer_id_for_order) - [`update_user_customer_id()`](#update_user_customer_id) - [`add_customer_id_to_user()`](#add_customer_id_to_user) - [`get_prepared_customer_data()`](#get_prepared_customer_data) --- #### `get_customer_id_by_user_id()` Get WCPay customer ID for the given WordPress user ID __Parameters__ - `$user_id` (`int`) The user ID to look for a customer ID with. __Return value__ `string?` WCPay customer ID or null if not found. --- #### `create_customer_for_user()` Create a customer and associate it with a WordPress user. __Parameters__ - `$user` (`WP_User`) User to create a customer for. - `$customer_data` (`array`) Customer data. __Return value__ `string` The created customer's ID --- #### `update_customer_for_user()` Update the customer details held on the WCPay server associated with the given WordPress user. __Parameters__ - `$customer_id` (`string`) WCPay customer ID. - `$user` (`WP_User`) WordPress user. - `$customer_data` (`array`) Customer data. This should be the return value of [`WC_Payments_Customer_Service_API::get_prepared_customer_data`](#get_prepared_customer_data) below. __Return value__ `string` The updated customer's ID. Can be different to the ID parameter if the customer was re-created. --- #### `set_default_payment_method_for_customer()` Sets a payment method as default for a customer. __Parameters__ - `$customer_id` (`string`) The customer ID. - `$payment_method_id` (`string`) The payment method ID. --- #### `get_payment_methods_for_customer()` Gets all payment methods for a customer. __Parameters__ - `$customer_id` (`string`) The customer ID. - `$type` (`string`) Type of payment methods to fetch. __Return value__ `array` Payment methods. --- #### `update_payment_method_with_billing_details_from_order()` Updates a customer payment method. __Parameters__ - `$payment_method_id` (`string`) The payment method ID. - `$order` (`WC_Order`) Order to be used on the update. __Return value__ `void` This method does not return any value. --- #### `clear_cached_payment_methods_for_user()` Clear payment methods cache for a user. __Parameters__ - `$user_id` (`int`) WC user ID. __Return value__ `void` This method does not return any value. --- #### `map_customer_data()` Given a WC_Order or WC_Customer, returns an array representing a Stripe customer object. At least one parameter has to not be null. __Parameters__ - `$wc_order` (`WC_Order`) The Woo order to parse. - `$wc_customer` (`WC_Customer`) The Woo customer to parse. __Return value__ `array` Customer data. public static function map_customer_data( WC_Order $wc_order, WC_Customer $wc_customer ) --- #### `delete_cached_payment_methods()` Delete all saved payment methods that are stored inside database cache driver. __Return value__ `void` This method does not return any value. --- #### `get_customer_id_for_order()` Get the WCPay customer ID associated with an order, or create one if none found. __Parameters__ - `$order` (`WC_Order`) WC Order object. __Return value__ `string|null` WCPay customer ID. --- #### `update_user_customer_id()` Updates the given user with the given WooCommerce Payments customer ID. __Parameters__ - `$user_id` (`int`) The WordPress user ID. - `$customer_id` (`string`) The WooCommerce Payments customer ID. __Return value__ `void` This method does not return any value. --- #### `add_customer_id_to_user()` Adds the WooComerce Payments customer ID found in the user session to the WordPress user as metadata. __Parameters__ - `$user_id` (`int`) The WordPress user ID. __Return value__ `void` This method does not return any value. --- #### `get_prepared_customer_data()` Prepares customer data to be used on 'Pay for Order' or 'Add Payment Method' pages. - Customer data is retrieved from order when on Pay for Order. - Customer data is retrieved from customer when on 'Add Payment Method'. __Return value__ `array|null` An array with customer data or nothing. includes/core/service/README.md 0000777 00000001560 15251156605 0012240 0 ustar 00 # WooCommerce Payments Services ## Purpose Expose WooCommerce Payments functionality through Services. Extensions should use these integration points provided in the `WCPay\Core` namespace, and should not use any methods from other classes, which are considered internal to WCPay. In this way, extensions will be able to use services in a reliable, and compatible manner. ## Services available - [`WC_Payments_Customer_Service_API`](customer-service.md) ## Adding New Services - Add the new service within `includes\core\service`. - Service should be in `WCPay\Core` namespace. - Add functions in your service that act as proxies to the internal service methods, or consider how they might change in the future. - Add tests in `tests\unit\core\service`. - Expose service via `WC_Payments`. [🔗 More about services within WooCommerce Payments Core](../CONTRIBUTING.md) includes/core/exceptions/server/request/class-server-request-exception.php 0000777 00000000522 15251156605 0023301 0 ustar 00 <?php /** * Class Server_Request_Exception * * @package WooCommerce\Payments */ namespace WCPay\Core\Exceptions\Server\Request; use WCPay\Exceptions\Base_Exception; defined( 'ABSPATH' ) || exit; /** * Default exception for throwing when using server request classes. */ class Server_Request_Exception extends Base_Exception { } includes/core/exceptions/server/request/class-invalid-request-parameter-exception.php 0000777 00000000600 15251156605 0025374 0 ustar 00 <?php /** * Class Invalid_Request_Parameter_Exception * * @package WooCommerce\Payments */ namespace WCPay\Core\Exceptions\Server\Request; use WCPay\Exceptions\Base_Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when immutable parameter inside request class is changed. */ class Invalid_Request_Parameter_Exception extends Base_Exception { } includes/core/exceptions/server/request/class-immutable-parameter-exception.php 0000777 00000000535 15251156605 0024246 0 ustar 00 <?php /** * Class Immutable_Param * * @package WooCommerce\Payments */ namespace WCPay\Core\Exceptions\Server\Request; use WCPay\Exceptions\Base_Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when someone tries to change an immutable param. */ class Immutable_Parameter_Exception extends Base_Exception { } includes/core/exceptions/server/request/class-extend-request-exception.php 0000777 00000000540 15251156605 0023262 0 ustar 00 <?php /** * Class Extend_Request_Exception * * @package WooCommerce\Payments */ namespace WCPay\Core\Exceptions\Server\Request; use WCPay\Exceptions\Base_Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when a request object is extended in wrong way. */ class Extend_Request_Exception extends Base_Exception { } includes/core/exceptions/server/response/class-server-response-exception.php 0000777 00000000527 15251156605 0023622 0 ustar 00 <?php /** * Class Server_Response_Exception * * @package WooCommerce\Payments */ namespace WCPay\Core\Exceptions\Server\Response; use WCPay\Exceptions\Base_Exception; defined( 'ABSPATH' ) || exit; /** * Exception for throwing an error when server response has exception. */ class Server_Response_Exception extends Base_Exception { } includes/class-session-rate-limiter.php 0000777 00000004763 15251156605 0014274 0 ustar 00 <?php /** * Class Session_Rate_Limiter * * @package WooCommerce\Payments */ namespace WCPay; defined( 'ABSPATH' ) || exit; // block direct access. /** * A wrapper class for keeping track of events in registries, and to trigger a rate limiter after a threshold. */ class Session_Rate_Limiter { /** * Key used in the session to store card declined transactions. * * @type string */ const SESSION_KEY_DECLINED_CARD_REGISTRY = 'wcpay_card_declined_registry'; /** * Key used to store the registry in the session * * @var string */ protected $key; /** * Number of elements in the registry needed to enable the rate limiter * * @var int */ protected $threshold; /** * Number of seconds the limiter is enabled for after the threshold is reached * * @var int */ protected $delay; /** * Session_Rate_Limiter constructor. * * @param string $key - Key for the registry. * @param int $threshold - Number of elements in the registry before enabling the limiter. * @param int $delay - Number of seconds the limiter will be in use after threshold is reached. */ public function __construct( $key, $threshold, $delay ) { $this->key = $key; $this->threshold = $threshold; $this->delay = $delay; } /** * Saves an event in an specified registry using a key. * If the number of events in the registry match the threshold, * a new rate limiter is enabled with the given delay. * * The registry of declined card attemps is cleaned after a new rate limiter is enabled. */ public function bump() { if ( ! isset( WC()->session ) ) { return; } $registry = WC()->session->get( $this->key ) ?? []; $registry[] = time(); WC()->session->set( $this->key, $registry ); } /** * Checks if the rate limiter is enabled. * * Returns a boolean. * * @return bool The rate limiter is in use. */ public function is_limited(): bool { if ( ! isset( WC()->session ) ) { return false; } if ( 'yes' === get_option( 'wcpay_session_rate_limiter_disabled_' . $this->key ) ) { return false; } $registry = WC()->session->get( $this->key ) ?? []; if ( ( is_countable( $registry ) ? count( $registry ) : 0 ) >= $this->threshold ) { $start_time_limiter = end( $registry ); $next_try_allowed_at = $start_time_limiter + $this->delay; $is_limited = time() <= $next_try_allowed_at; if ( ! $is_limited ) { WC()->session->set( $this->key, [] ); } return $is_limited; } return false; } } includes/class-wc-payments-action-scheduler-service.php 0000777 00000022231 15251156605 0017337 0 ustar 00 <?php /** * WC_Payments_Action_Scheduler_Service class * * @package WooCommerce\Payments */ use WCPay\Compatibility_Service; use WCPay\Constants\Order_Mode; defined( 'ABSPATH' ) || exit; /** * Class which handles setting up all ActionScheduler hooks. */ class WC_Payments_Action_Scheduler_Service { const GROUP_ID = 'woocommerce_payments'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * WC_Payments_Order_Service instance for updating order statuses. * * @var WC_Payments_Order_Service */ private $order_service; /** * Compatibility service instance for updating compatibility data. * * @var Compatibility_Service */ private $compatibility_service; /** * Constructor for WC_Payments_Action_Scheduler_Service. * * @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client. * @param WC_Payments_Order_Service $order_service - Order Service. * @param Compatibility_Service $compatibility_service - Compatibility service instance. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Order_Service $order_service, Compatibility_Service $compatibility_service ) { $this->payments_api_client = $payments_api_client; $this->order_service = $order_service; $this->compatibility_service = $compatibility_service; } /** * Initialize all hooks. * * @return void */ public function init_hooks() { $this->ensure_recurring_actions(); $this->add_action_scheduler_hooks(); } /** * Ensure all recurring actions are scheduled. * * @return void */ public function ensure_recurring_actions() { if ( function_exists( 'as_supports' ) && as_supports( 'ensure_recurring_actions_hook' ) ) { // Preferred since AS v3.9.3 (WC 10.1): runs periodically in the background. add_action( 'action_scheduler_ensure_recurring_actions', [ $this, 'schedule_recurring_actions' ] ); } elseif ( is_admin() ) { // Backward compatible fallback: runs on every admin request. $this->schedule_recurring_actions(); } } /** * Schedule all recurring actions. * * If the action is already scheduled, it won't be rescheduled again. * * @return void */ public function schedule_recurring_actions() { // Check if Action Scheduler is available. if ( ! function_exists( 'as_schedule_recurring_action' ) || ! function_exists( 'as_has_scheduled_action' ) ) { return; } if ( ! as_has_scheduled_action( WC_Payments_Account::STORE_SETUP_SYNC_ACTION ) ) { // Delay the first run by 10 to 60 seconds (randomly) so it doesn't occur in the same request. // The random delay is to avoid large number of sites hitting the API at the same time if they all // get updated at the same time (e.g. via WP cron). as_schedule_recurring_action( time() + wp_rand( 10, 60 ), 6 * HOUR_IN_SECONDS, WC_Payments_Account::STORE_SETUP_SYNC_ACTION, [], self::GROUP_ID, true ); } } /** * Attach hooks for all ActionScheduler actions. * * @return void */ public function add_action_scheduler_hooks() { add_action( 'wcpay_track_new_order', [ $this, 'track_new_order_action' ] ); add_action( 'wcpay_track_update_order', [ $this, 'track_update_order_action' ] ); add_action( WC_Payments_Order_Service::ADD_FEE_BREAKDOWN_TO_ORDER_NOTES, [ $this->order_service, 'add_fee_breakdown_to_order_notes' ], 10, 3 ); add_action( Compatibility_Service::UPDATE_COMPATIBILITY_DATA, [ $this->compatibility_service, 'update_compatibility_data_hook' ], 10, 0 ); } /** * This function is a hook that will be called by ActionScheduler when an order is created. * It will make a request to the Payments API to track this event. * * @param array $order_id The ID of the order that has been created. * * @return bool */ public function track_new_order_action( $order_id ) { return $this->track_order( $order_id, false ); } /** * This function is a hook that will be called by ActionScheduler when an order is updated. * It will make a request to the Payments API to track this event. * * @param int $order_id The ID of the order which has been updated. * * @return bool */ public function track_update_order_action( $order_id ) { return $this->track_order( $order_id, true ); } /** * Track an order by making a request to the Payments API. * * @param mixed $order_id The ID of the order which has been updated/created. * @param bool $is_update Is this an update event. If false, it is assumed this is a creation event. * * @return bool */ private function track_order( $order_id, $is_update = false ) { // Get the order details. $order = wc_get_order( $order_id ); if ( ! $order ) { return false; } // If we do not have a valid payment method for this order, don't send the request. $payment_method = $this->order_service->get_payment_method_id_for_order( $order ); if ( empty( $payment_method ) ) { return false; } $order_mode = $order->get_meta( WC_Payments_Order_Service::WCPAY_MODE_META_KEY ); if ( $order_mode ) { $current_mode = WC_Payments::mode()->is_test() ? Order_Mode::TEST : Order_Mode::PRODUCTION; if ( $current_mode !== $order_mode ) { // If mode doesn't match make sure to stop order tracking to prevent order tracking issues. // False will be returned so maybe future crons will have correct mode. return false; } } // Send the order data to the Payments API to track it. $response = $this->payments_api_client->track_order( array_merge( $order->get_data(), [ '_payment_method_id' => $payment_method, '_stripe_customer_id' => $this->order_service->get_customer_id_for_order( $order ), '_wcpay_mode' => $order_mode, ] ), $is_update ); if ( 'success' === $response['result'] && ! $is_update ) { // Update the metadata to reflect that the order creation event has been fired. $order->add_meta_data( '_new_order_tracking_complete', 'yes' ); $order->save_meta_data(); } return ( 'success' === ( $response['result'] ?? null ) ); } /** * Schedule an action scheduler job. * * Also, unschedules (replaces) any previous instances of the same job. * This prevents duplicate jobs, for example when multiple events fire as part of the order update process. * We will only replace a job which has the same $hook, $args AND $group. * * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. * @param array $args Optional. An array containing the arguments to be passed to the hook. * Defaults to an empty array. * @param string $group Optional. The AS group the action will be created under. * Defaults to 'woocommerce_payments'. * * @return void */ public function schedule_job( int $timestamp, string $hook, array $args = [], string $group = self::GROUP_ID ) { // The `action_scheduler_init` hook was introduced in ActionScheduler 3.5.5 (WooCommerce 7.9.0). if ( version_compare( WC()->version, '7.9.0', '>=' ) ) { // If the ActionScheduler is already initialized, schedule the job. if ( did_action( 'action_scheduler_init' ) ) { $this->schedule_action_and_prevent_duplicates( $timestamp, $hook, $args, $group ); } else { // The ActionScheduler is not initialized yet; we need to schedule the job when it fires the init hook. add_action( 'action_scheduler_init', function () use ( $timestamp, $hook, $args, $group ) { $this->schedule_action_and_prevent_duplicates( $timestamp, $hook, $args, $group ); } ); } } else { $this->schedule_action_and_prevent_duplicates( $timestamp, $hook, $args, $group ); } } /** * Checks to see if there is a Pending action with the same hook already. * * @param string $hook Hook name. * * @return bool */ public function pending_action_exists( string $hook ): bool { return as_has_scheduled_action( $hook, [], self::GROUP_ID ); } /** * Schedule an action while unscheduling any scheduled actions that are exactly the same. * * We will look for scheduled actions with the same name, args and group when unscheduling. * * @param int $timestamp When the action will run. * @param string $action The action name to schedule. * @param array $args Optional. An array containing the arguments to be passed to the action. * Defaults to an empty array. * @param string $group Optional. The ActionScheduler group the action will be created under. * Defaults to 'woocommerce_payments'. * * @return void */ private function schedule_action_and_prevent_duplicates( int $timestamp, string $action, array $args = [], string $group = self::GROUP_ID ) { // Unschedule any previously scheduled actions with the same name, args, and group combination. // It is more efficient/performant to check if the action is already scheduled before unscheduling it. // @see https://github.com/Automattic/woocommerce-payments/issues/6662. if ( as_has_scheduled_action( $action, $args, $group ) ) { as_unschedule_action( $action, $args, $group ); } as_schedule_single_action( $timestamp, $action, $args, $group ); } } includes/woopay/class-woopay-order-status-sync.php 0000777 00000013551 15251156605 0016450 0 ustar 00 <?php /** * Class WooPay_Webhooks * * @package WooCommerce\Payments */ namespace WCPay\WooPay; use WC_Payments_Account; use WC_Payments_API_Client; use WCPay\Exceptions\API_Exception; defined( 'ABSPATH' ) || exit; /** * This class introduces webhooks to delivery order updates to the associated * orders in the woopay. * * WooPay Webhooks are enqueued to their associated actions, delivered, and logged. */ class WooPay_Order_Status_Sync { const WCPAY_WEBHOOK_WOOPAY_ORDER_STATUS_CHANGED = 'wcpay_webhook_platform_checkout_order_status_changed'; /** * WC_Payments_Account instance to get information about the account * * @var WC_Payments_Account */ private $account; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ protected $payments_api_client; /** * Setup webhook for the WooPay Order Status Sync. * * @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client. * @param WC_Payments_Account $account - WooCommerce Payments account. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account ) { $this->payments_api_client = $payments_api_client; $this->account = $account; add_filter( 'woocommerce_webhook_topic_hooks', [ __CLASS__, 'add_topics' ], 20, 2 ); add_filter( 'woocommerce_webhook_payload', [ __CLASS__, 'create_payload' ], 10, 4 ); add_filter( 'woocommerce_valid_webhook_resources', [ __CLASS__, 'add_resource' ], 10, 1 ); add_filter( 'woocommerce_valid_webhook_events', [ __CLASS__, 'add_event' ], 10, 1 ); add_action( 'woocommerce_order_status_changed', [ __CLASS__, 'send_webhook' ], 10, 3 ); add_action( 'admin_init', [ $this, 'maybe_create_woopay_order_webhook' ], 10 ); } /** * Return the webhook name. * * @return string */ private static function get_webhook_name() { return __( 'WooPayments woopay order status sync', 'woocommerce-payments' ); } /** * Maybe create the WooPay webhook under certain conditions. */ public function maybe_create_woopay_order_webhook() { if ( ! current_user_can( 'manage_woocommerce' ) || self::is_webhook_created() ) { return; } if ( ! $this->account->is_stripe_account_valid() || $this->account->is_account_under_review() || $this->account->is_account_rejected() ) { return; } $this->register_webhook(); } /** * Return true if webhook was already created. * * @return bool */ private static function is_webhook_created() { return ! empty( self::get_webhook() ); } /** * Return array with the webhook id for the woopay order status sync. * * @return array */ public static function get_webhook() { $data_store = \WC_Data_Store::load( 'webhook' ); $args = [ 'search' => self::get_webhook_name(), 'status' => 'active', 'limit' => 1, ]; $webhooks = $data_store->search_webhooks( $args ); return $webhooks; } /** * Register the webhook on WooCommerce. * * @return void */ private function register_webhook() { $webhook = new \WC_Webhook(); $webhook->set_name( self::get_webhook_name() ); $webhook->set_user_id( get_current_user_id() ); $webhook->set_topic( 'order.status_changed' ); $webhook->set_secret( wp_generate_password( 50, false ) ); $webhook->set_delivery_url( WooPay_Utilities::get_woopay_rest_url( 'merchant-notification' ) ); $webhook->set_status( 'active' ); $webhook->save(); try { $this->payments_api_client->update_woopay( [ 'webhook_secret' => $webhook->get_secret() ] ); } catch ( API_Exception $e ) { $webhook->delete(); } } /** * Add order webhook topic * * @param array $topic_hooks List of WooCommerce's standard webhook topics and hooks. */ public static function add_topics( $topic_hooks ) { $topic_hooks['order.status_changed'][] = self::WCPAY_WEBHOOK_WOOPAY_ORDER_STATUS_CHANGED; return $topic_hooks; } /** * Setup payload for the webhook delivery. * * @param array $payload Data to be sent out by the webhook. * @param string $resource_name Type/name of the resource. * @param integer $resource_id ID of the resource. * @param integer $id ID of the webhook. */ public static function create_payload( $payload, $resource_name, $resource_id, $id ) { $webhook = wc_get_webhook( $id ); if ( ! $webhook ) { return $payload; } if ( 0 !== strpos( $webhook->get_delivery_url(), WooPay_Utilities::get_woopay_rest_url( 'merchant-notification' ) ) ) { // This is not a WooPay webhook, so we don't need to modify the payload. return $payload; } return [ 'blog_id' => \Jetpack_Options::get_option( 'id' ), 'order_id' => $resource_id, 'order_status' => $payload['status'], ]; } /** * Add webhook resource for order. * * @param array $resources List of available resources. */ public static function add_resource( $resources ) { $resources[] = 'order'; return $resources; } /** * Undocumented function * * @param array $topic_events List of available topic events. */ public static function add_event( $topic_events ) { $topic_events[] = 'status_changed'; return $topic_events; } /** * Trigger webhook delivery. * * @param int $order_id Order id. * @param string $previous_status the old WooCommerce order status. * @param string $next_status the new WooCommerce order status. * @return void */ public static function send_webhook( $order_id, $previous_status, $next_status ) { $order = wc_get_order( $order_id ); if ( $order->get_meta( 'is_woopay' ) ) { do_action( self::WCPAY_WEBHOOK_WOOPAY_ORDER_STATUS_CHANGED, $order_id, $next_status ); } } /** * Removes the webhook if woopay is disabled. * * @return void */ public static function remove_webhook() { if ( self::is_webhook_created() ) { $webhook_id = self::get_webhook()[0]; $webhook = new \WC_Webhook( $webhook_id ); $webhook->delete(); } } } includes/woopay/services/class-checkout-service.php 0000777 00000007410 15251156605 0016611 0 ustar 00 <?php /** * Class Create_And_Confirm_Intention_Test * * @package Checkout_Service */ namespace WCPay\WooPay\Service; use WC_Payments_Features; use WCPay\Core\Server\Request; use WCPay\Core\Server\Request\WooPay_Create_And_Confirm_Intention; use WCPay\Core\Server\Request\WooPay_Create_And_Confirm_Setup_Intention; use WCPay\Payment_Information; /** * Checkout service class. */ class Checkout_Service { /** * Create woopay request from base create and confirm request. * * @param Request $base_request Base request. * @param Payment_Information $payment_information Using saved payment method. * * @return WooPay_Create_And_Confirm_Intention * @throws \WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception */ public function create_intention_request( Request $base_request, Payment_Information $payment_information ) { $request = WooPay_Create_And_Confirm_Intention::extend( $base_request ); $request->set_has_woopay_subscription( '1' === $payment_information->get_order()->get_meta( '_woopay_has_subscription' ) ); $request->set_save_payment_method_to_platform( $payment_information->should_save_payment_method_to_platform() ); $request->set_is_platform_payment_method( $this->is_platform_payment_method( $payment_information ) ); return $request; } /** * Create woopay setup and confirm intent request from base create and confirm request. * * @param Request $base_request Base request. * @param Payment_Information $payment_information Using saved payment method. * @param bool $save_in_platform_account Should save in platform account. * @param bool $save_payment_method_to_platform Should save in platform. * * @return WooPay_Create_And_Confirm_Setup_Intention * @throws \WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception */ public function create_and_confirm_setup_intention_request( Request $base_request, Payment_Information $payment_information, bool $save_in_platform_account, bool $save_payment_method_to_platform ) { $request = WooPay_Create_And_Confirm_Setup_Intention::extend( $base_request ); $request->set_save_in_platform_account( $save_in_platform_account ); $request->set_save_payment_method_to_platform( $save_payment_method_to_platform ); $request->set_is_platform_payment_method( $this->is_platform_payment_method( $payment_information ) ); return $request; } /** * Determine if current payment method is a platform payment method. * * @param Payment_Information $payment_information Payment information object used to determine if a saved payment method is being used as well as helps to determine * if stripe platform account should be used or not. * * @return boolean True if it is a platform payment method. */ public function is_platform_payment_method( Payment_Information $payment_information ) { // Return false for express checkout method. if ( isset( $_POST['express_payment_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return false; } $should_use_stripe_platform = \WC_Payments::get_payment_gateway_by_id( $payment_information->get_payment_method_stripe_id() )->should_use_stripe_platform_on_checkout_page(); // Make sure the payment method being charged was created in the platform. if ( ! $payment_information->is_using_saved_payment_method() && $should_use_stripe_platform ) { // This payment method was created under the platform account. return true; } return false; } /** * Load filters. * * @return void */ public function init() { add_filter( 'wcpay_create_and_confirm_intent_request', [ $this, 'create_intention_request' ], 10, 3 ); add_filter( 'wcpay_create_and_confirm_setup_intention_request', [ $this, 'create_and_confirm_setup_intention_request' ], 10, 4 ); } } includes/woopay/class-woopay-store-api-session-handler.php 0000777 00000007457 15251156605 0020051 0 ustar 00 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName /** * Class SessionHandler * * This is a copy of the Automattic\WooCommerce\StoreApi\SessionHandler class with the addition of an `init_session_cookie` method. * * @package WooCommerce\Payments */ namespace WCPay\Platform_Checkout; use Automattic\Jetpack\Constants; use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken; use WC_Session; defined( 'ABSPATH' ) || exit; /** * SessionHandler class */ final class SessionHandler extends WC_Session { /** * Token from HTTP headers. * * @var string */ protected $token; /** * Table name for session data. * * @var string Custom session table name */ protected $table; /** * Expiration timestamp. * * @var int */ protected $session_expiration; /** * Constructor for the session class. */ public function __construct() { $this->token = wc_clean( wp_unslash( $_SERVER['HTTP_CART_TOKEN'] ?? '' ) ); $this->table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions'; } /** * Note: This method was added to the original class for compatibility with WooPay. */ public function init_session_cookie() { $this->init(); } /** * Init hooks and session data. */ public function init() { $this->init_session_from_token(); add_action( 'shutdown', [ $this, 'save_data' ], 20 ); } /** * Return true if the current user has an active session,. * * @return bool */ public function has_session() { return ! empty( $this->token ); } /** * Returns the session. * * @param string $customer_id Customer ID. * @param mixed $default Default session value. * * @return string|array|bool */ public function get_session( $customer_id, $default = false ) { global $wpdb; // This mimics behaviour from default WC_Session_Handler class. There will be no sessions retrieved while WP setup is due. if ( Constants::is_defined( 'WP_SETUP_CONFIG' ) ) { return false; } $value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->table WHERE session_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $customer_id ) ); if ( is_null( $value ) ) { $value = $default; } return maybe_unserialize( $value ); } /** * Save data and delete user session. */ public function save_data() { // Dirty if something changed - prevents saving nothing new. if ( $this->_dirty ) { global $wpdb; $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "INSERT INTO $this->table (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d) ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)", $this->_customer_id, maybe_serialize( $this->_data ), $this->session_expiration ) ); wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->session_expiration - time() ); $this->_dirty = false; } } /** * Get session data. * * @return array */ public function get_session_data() { return $this->has_session() ? (array) $this->get_session( $this->get_customer_id(), [] ) : []; } /** * Process the token header to load the correct session. */ protected function init_session_from_token() { $payload = JsonWebToken::get_parts( $this->token )->payload; $this->_customer_id = $payload->user_id; $this->session_expiration = $payload->exp; $this->_data = (array) $this->get_session( $this->_customer_id, [] ); } /** * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call. * * @return string */ private function get_cache_prefix() { return \WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP ); } } includes/woopay/class-woopay-session.php 0000777 00000122140 15251156605 0014520 0 ustar 00 <?php /** * Class WooPay_Session. * * @package WooCommerce\Payments */ namespace WCPay\WooPay; use Automattic\Jetpack\Connection\Rest_Authentication; use Automattic\WooCommerce\StoreApi\RoutesController; use Automattic\WooCommerce\StoreApi\StoreApi; use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken; use Jetpack_Options; use WCPay\Blocks_Data_Extractor; use WCPay\Logger; use WCPay\Platform_Checkout\SessionHandler; use WCPay\Platform_Checkout\WooPay_Store_Api_Token; use WCPay\WooPay\WooPay_Scheduler; use WC_Customer; use WC_Payments; use WC_Payments_Customer_Service; use WC_Payments_Features; use WCPay\MultiCurrency\MultiCurrency; use WP_REST_Request; /** * Class responsible for handling woopay sessions. * This class should be loaded as soon as possible so the correct session is loaded. * So don't load it in the WC_Payments::init() function. */ class WooPay_Session { const STORE_API_NAMESPACE_PATTERN = '@^(wc/store(/v[\d]+)?|store-api)$@'; const WOOPAY_SESSION_KEY = 'woopay-user-data'; /** * Order ID used for error handling. * * @var int|null */ private static $checkout_error_order_id = null; /** * Whether the error handler has been registered. * * @var bool */ private static $is_error_handler_registered = false; /** * Init the hooks. * * @return void */ public static function init() { add_filter( 'determine_current_user', [ __CLASS__, 'determine_current_user_for_woopay' ], 20 ); add_filter( 'woocommerce_session_handler', [ __CLASS__, 'add_woopay_store_api_session_handler' ], 20 ); add_action( 'woocommerce_order_payment_status_changed', [ __CLASS__, 'woopay_order_payment_status_changed' ] ); add_action( 'woopay_restore_order_customer_id', [ __CLASS__, 'restore_order_customer_id_from_requests_with_verified_email' ] ); add_filter( 'woocommerce_order_needs_payment', [ __CLASS__, 'woopay_trial_subscriptions_handler' ], 20, 3 ); add_action( 'woocommerce_store_api_checkout_order_processed', [ __CLASS__, 'catch_woopay_checkout_errors' ], 1, 1 ); register_deactivation_hook( WCPAY_PLUGIN_FILE, [ __CLASS__, 'run_and_remove_woopay_restore_order_customer_id_schedules' ] ); add_filter( 'automatewoo/referrals/referred_order_advocate', [ __CLASS__, 'automatewoo_refer_a_friend_referral_from_parameter' ] ); } /** * This filter is used to add a custom session handler before processing Store API request callbacks. * This is only necessary because the Store API SessionHandler currently doesn't provide an `init_session_cookie` method. * * @param string $default_session_handler The default session handler class name. * * @return string The session handler class name. */ public static function add_woopay_store_api_session_handler( $default_session_handler ) { $cart_token = wc_clean( wp_unslash( $_SERVER['HTTP_CART_TOKEN'] ?? null ) ); if ( $cart_token && self::is_request_from_woopay() && \WC_Payments_Utils::is_store_api_request() && class_exists( JsonWebToken::class ) && JsonWebToken::validate( $cart_token, '@' . wp_salt() ) ) { return SessionHandler::class; } return $default_session_handler; } /** * Sets the current user as the user sent via the api from WooPay if present. * * @param \WP_User|null|int $user user to be used during the request. * * @return \WP_User|null|int */ public static function determine_current_user_for_woopay( $user ) { if ( ! self::is_request_from_woopay() || ! \WC_Payments_Utils::is_store_api_request() ) { return $user; } if ( ! self::is_woopay_enabled() ) { return $user; } // Validate that the request is signed properly. if ( ! self::has_valid_request_signature() ) { Logger::log( __( 'WooPay request is not signed correctly.', 'woocommerce-payments' ) ); wp_die( esc_html__( 'WooPay request is not signed correctly.', 'woocommerce-payments' ), 401 ); } add_filter( 'wcpay_is_woopay_store_api_request', '__return_true' ); $cart_token_user_id = self::get_user_id_from_cart_token(); if ( null === $cart_token_user_id ) { return $user; } return $cart_token_user_id; } /** * Returns the user ID from the cart token. * * @return int|null The User ID or null if there's no cart token in the request. */ public static function get_user_id_from_cart_token() { $payload = self::get_payload_from_cart_token(); if ( null === $payload ) { return null; } $session_handler = new SessionHandler(); $session_data = $session_handler->get_session( $payload->user_id ); $customer = maybe_unserialize( $session_data['customer'] ); // If the token is already authenticated, return the customer ID. if ( is_numeric( $customer['id'] ) && intval( $customer['id'] ) > 0 ) { return intval( $customer['id'] ); } $woopay_verified_email_address = self::get_woopay_verified_email_address(); $enabled_adapted_extensions = get_option( WooPay_Scheduler::ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME, [] ); // If the email is verified on WooPay, matches session email (set during the redirection), // and the store has an adapted extension installed, // return the user to get extension data without authentication. if ( ( is_countable( $enabled_adapted_extensions ) ? count( $enabled_adapted_extensions ) : 0 ) > 0 && null !== $woopay_verified_email_address && ! empty( $customer['email'] ) ) { $user = get_user_by( 'email', $woopay_verified_email_address ); if ( $woopay_verified_email_address === $customer['email'] && $user ) { // Remove Gift Cards session cache to load account gift cards. add_filter( 'woocommerce_gc_account_session_timeout_minutes', '__return_false' ); return $user->ID; } } return null; } /** * Update order data for extensions which uses cookies, * also prevent set order customer ID on requests with * email verified to skip the login screen on the TYP. * After 10 minutes, the customer ID will be restored * and the user will need to login to access the TYP. * * @param int $order_id The order ID being updated. */ public static function woopay_order_payment_status_changed( $order_id ) { if ( ! self::is_woopay_enabled() ) { return; } if ( ! self::is_request_from_woopay() || ! \WC_Payments_Utils::is_store_api_request() ) { return; } $woopay_adapted_extensions = new WooPay_Adapted_Extensions(); $woopay_adapted_extensions->update_order_extension_data( $order_id ); $woopay_verified_email_address = self::get_woopay_verified_email_address(); if ( null === $woopay_verified_email_address ) { return; } $enabled_adapted_extensions = get_option( WooPay_Scheduler::ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME, [] ); if ( ( is_countable( $enabled_adapted_extensions ) ? count( $enabled_adapted_extensions ) : 0 ) === 0 ) { return; } $payload = self::get_payload_from_cart_token(); if ( null === $payload ) { return; } $order = wc_get_order( $order_id ); // Guest users user_id on the cart token payload looks like "t_hash" and the order // customer id is 0, logged in users is the real user id in both cases. $user_is_logged_in = $payload->user_id === $order->get_customer_id(); if ( ! $user_is_logged_in && $woopay_verified_email_address === $order->get_billing_email() ) { $order->add_meta_data( 'woopay_merchant_customer_id', $order->get_customer_id(), true ); $order->set_customer_id( 0 ); $order->save(); wp_schedule_single_event( time() + 10 * MINUTE_IN_SECONDS, 'woopay_restore_order_customer_id', [ $order_id ] ); } } /** * Restore the order customer ID after 10 minutes * on requests with email verified. * * @param \WC_Order $order_id The order ID being updated. */ public static function restore_order_customer_id_from_requests_with_verified_email( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order->meta_exists( 'woopay_merchant_customer_id' ) ) { return; } $order->set_customer_id( $order->get_meta( 'woopay_merchant_customer_id' ) ); $order->delete_meta_data( 'woopay_merchant_customer_id' ); $order->save(); } /** * Restore all WooPay verified email orders customer ID * and disable the schedules when plugin is disabled. */ public static function run_and_remove_woopay_restore_order_customer_id_schedules() { // WooCommerce is disabled when disabling WCPay. if ( ! function_exists( 'wc_get_orders' ) ) { return; } $args = [ 'meta_key' => 'woopay_merchant_customer_id', //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'return' => 'ids', ]; $order_ids = wc_get_orders( $args ); if ( ! empty( $order_ids ) ) { foreach ( $order_ids as $order_id ) { self::restore_order_customer_id_from_requests_with_verified_email( $order_id ); } } wp_clear_scheduled_hook( 'woopay_restore_order_customer_id' ); } /** * Fix for AutomateWoo - Refer A Friend Add-on * plugin when using link referrals. * * @param int $advocate_id The advocate ID. * * @return false|int|mixed The advocate ID or false if the request is not from WooPay. */ public static function automatewoo_refer_a_friend_referral_from_parameter( $advocate_id ) { if ( ! self::is_request_from_woopay() || ! \WC_Payments_Utils::is_store_api_request() ) { return $advocate_id; } if ( ! self::is_woopay_enabled() ) { return $advocate_id; } if ( empty( $_GET['automatewoo_referral_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return false; } $automatewoo_referral = (int) wc_clean( wp_unslash( $_GET['automatewoo_referral_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification return $automatewoo_referral; } /** * Process trial subscriptions for WooPay. * * @param bool $needs_payment If the order needs payment. * @param \WC_Order $order The order. * @param array $valid_order_statuses The valid order statuses. */ public static function woopay_trial_subscriptions_handler( $needs_payment, $order, $valid_order_statuses ) { if ( ! self::is_request_from_woopay() || ! \WC_Payments_Utils::is_store_api_request() ) { return $needs_payment; } if ( ! self::is_woopay_enabled() ) { return $needs_payment; } if ( ! class_exists( 'WC_Subscriptions_Cart' ) || $order->get_total() > 0 ) { return $needs_payment; } if ( \WC_Subscriptions_Cart::cart_contains_subscription() ) { return true; } return $needs_payment; } /** * Returns the payload from a cart token. * * @return object|null The cart token payload if it's valid. */ private static function get_payload_from_cart_token() { if ( ! isset( $_SERVER['HTTP_CART_TOKEN'] ) ) { return null; } if ( ! class_exists( JsonWebToken::class ) ) { return null; } $cart_token = wc_clean( wp_unslash( $_SERVER['HTTP_CART_TOKEN'] ) ); if ( $cart_token && JsonWebToken::validate( $cart_token, '@' . wp_salt() ) ) { $payload = JsonWebToken::get_parts( $cart_token )->payload; if ( empty( $payload ) ) { return null; } // Store API namespace is used as the token issuer. if ( ! preg_match( self::STORE_API_NAMESPACE_PATTERN, $payload->iss ) ) { return null; } return $payload; } return null; } /** * Returns the encrypted session request for the frontend. * * @return array The encrypted session request or an empty array if the server is not eligible for encryption. */ public static function get_frontend_init_session_request() { if ( ! extension_loaded( 'openssl' ) || ! function_exists( 'openssl_encrypt' ) ) { return []; } // phpcs:disable WordPress.Security.NonceVerification.Missing $order_id = ! empty( $_POST['order_id'] ) ? absint( wp_unslash( $_POST['order_id'] ) ) : null; $key = ! empty( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : null; $billing_email = ! empty( $_POST['billing_email'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_email'] ) ) : null; // phpcs:enable // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, Generic.Arrays.DisallowLongArraySyntax.Found $appearance = ! empty( $_POST['appearance'] ) ? self::array_map_recursive( array( __CLASS__, 'sanitize_string' ), $_POST['appearance'] ) : null; $session = self::get_init_session_request( $order_id, $key, $billing_email, null, $appearance ); return WooPay_Utilities::encrypt_and_sign_data( $session ); } /** * Retrieves cart data from the current session. * * If the request doesn't come from WooPay, this uses the same strategy in * `hydrate_from_api` on the Checkout Block to retrieve cart data. * * @param bool $is_pay_for_order Whether the request is for a pay-for-order session. * @param int|null $order_id Pay-for-order order ID. * @param string|null $key Pay-for-order key. * @param string|null $billing_email Pay-for-order billing email. * @param WP_REST_Request|null $woopay_request The WooPay request object. * * @return array The cart data. */ private static function get_cart_data( $is_pay_for_order, $order_id, $key, $billing_email, $woopay_request ) { if ( ! $woopay_request ) { return ! $is_pay_for_order ? rest_preload_api_request( [], '/wc/store/v1/cart' )['/wc/store/v1/cart']['body'] : rest_preload_api_request( [], '/wc/store/v1/order/' . rawurlencode( $order_id ) . '?key=' . rawurlencode( $key ) . '&billing_email=' . rawurlencode( $billing_email ) )[ '/wc/store/v1/order/' . rawurlencode( $order_id ) . '?key=' . rawurlencode( $key ) . '&billing_email=' . rawurlencode( $billing_email ) ]['body']; } $cart_request = new WP_REST_Request( 'GET', '/wc/store/v1/cart' ); $cart_request->set_header( 'Cart-Token', $woopay_request->get_header( 'cart_token' ) ); return rest_do_request( $cart_request )->get_data(); } /** * Retrieves checkout data from the current session. * * If the request doesn't come from WooPay, this uses the same strategy in * `hydrate_from_api` on the Checkout Block to retrieve checkout data. * * @param WP_REST_Request $woopay_request The WooPay request object. * @return mixed The checkout data. */ private static function get_checkout_data( $woopay_request ) { add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' ); if ( ! $woopay_request ) { $preloaded_checkout_data = rest_preload_api_request( [], '/wc/store/v1/checkout' ); $checkout_data = isset( $preloaded_checkout_data['/wc/store/v1/checkout'] ) ? $preloaded_checkout_data['/wc/store/v1/checkout']['body'] : ''; } else { $checkout_request = new WP_REST_Request( 'GET', '/wc/store/v1/checkout' ); $checkout_request->set_header( 'Cart-Token', $woopay_request->get_header( 'cart_token' ) ); $checkout_data = rest_do_request( $checkout_request )->get_data(); } remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' ); return $checkout_data; } /** * Retrieves the user email from the current session. * * @param \WP_User $user The user object. * @return string The user email. */ public static function get_user_email( $user ) { if ( ! empty( $_POST['email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return sanitize_email( wp_unslash( $_POST['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification } if ( ! empty( $_GET['email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return sanitize_email( wp_unslash( $_GET['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification } if ( ! empty( $_POST['encrypted_data'] ) && is_array( $_POST['encrypted_data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash $decrypted_data = WooPay_Utilities::decrypt_signed_data( $_POST['encrypted_data'] ); if ( ! empty( $decrypted_data['user_email'] ) ) { return sanitize_email( wp_unslash( $decrypted_data['user_email'] ) ); } } // Get the email from the customer object if it's available. if ( ! empty( WC()->customer ) ) { $billing_email = WC()->customer->get_billing_email(); if ( ! empty( $billing_email ) ) { return $billing_email; } $customer_email = WC()->customer->get_email(); if ( ! empty( $customer_email ) ) { return $customer_email; } } // As a last resort, we try to get the email from the customer logged in the store. if ( $user->exists() ) { return $user->user_email; } return ''; } /** * Returns the initial session request data. * * @param int|null $order_id Pay-for-order order ID. * @param string|null $key Pay-for-order key. * @param string|null $billing_email Pay-for-order billing email. * @param WP_REST_Request|null $woopay_request The WooPay request object. * @param array $appearance Merchant appearance. * @return array The initial session request data without email and user_session. */ public static function get_init_session_request( $order_id = null, $key = null, $billing_email = null, $woopay_request = null, $appearance = null ) { $user = wp_get_current_user(); $is_pay_for_order = null !== $order_id; $order = wc_get_order( $order_id ); $customer_id = WC_Payments::get_customer_service()->get_customer_id_by_user_id( $user->ID ); if ( null === $customer_id ) { // create customer. $customer_data = WC_Payments_Customer_Service::map_customer_data( null, new WC_Customer( $user->ID ) ); $customer_id = WC_Payments::get_customer_service()->create_customer_for_user( $user, $customer_data ); } if ( WC_Payments_Features::is_customer_multi_currency_enabled() && 0 !== $user->ID ) { // Multicurrency selection is stored on user meta when logged in and WC session when logged out. // This code just makes sure that currency selection is available on WC session for WooPay. $currency = get_user_meta( $user->ID, MultiCurrency::CURRENCY_META_KEY, true ); $currency_code = strtoupper( $currency ); if ( ! empty( $currency_code ) && WC()->session ) { WC()->session->set( MultiCurrency::CURRENCY_SESSION_KEY, $currency_code ); } } $account_id = WC_Payments::get_account_service()->get_stripe_account_id(); $site_logo_id = get_theme_mod( 'custom_logo' ); $site_logo_url = $site_logo_id ? ( wp_get_attachment_image_src( $site_logo_id, 'full' )[0] ?? '' ) : ''; $woopay_store_logo = WC_Payments::get_gateway()->get_option( 'platform_checkout_store_logo' ); $store_logo = $site_logo_url; if ( ! empty( $woopay_store_logo ) ) { $store_logo = get_rest_url( null, 'wc/v3/payments/file/' . $woopay_store_logo ); } include_once WCPAY_ABSPATH . 'includes/compat/blocks/class-blocks-data-extractor.php'; $blocks_data_extractor = new Blocks_Data_Extractor(); $cart_data = self::get_cart_data( $is_pay_for_order, $order_id, $key, $billing_email, $woopay_request ); $checkout_data = self::get_checkout_data( $woopay_request ); $email = self::get_user_email( $user ); if ( $woopay_request ) { $order_id = $checkout_data['order_id'] ?? null; } $request = [ 'wcpay_version' => WCPAY_VERSION_NUMBER, 'user_id' => $user->ID, 'customer_id' => $customer_id, 'session_nonce' => self::create_woopay_nonce( $user->ID ), 'store_api_token' => self::init_store_api_token(), 'email' => $email, 'store_data' => [ 'store_name' => get_bloginfo( 'name' ), 'store_logo' => $store_logo, 'custom_message' => self::get_formatted_custom_terms(), 'blog_id' => Jetpack_Options::get_option( 'id' ), 'blog_url' => get_site_url(), 'blog_checkout_url' => ! $is_pay_for_order ? wc_get_checkout_url() : $order->get_checkout_payment_url(), 'blog_shop_url' => get_permalink( wc_get_page_id( 'shop' ) ), 'blog_timezone' => wp_timezone_string(), 'store_api_url' => self::get_store_api_url(), 'account_id' => $account_id, 'test_mode' => WC_Payments::mode()->is_test(), 'capture_method' => empty( WC_Payments::get_gateway()->get_option( 'manual_capture' ) ) || 'no' === WC_Payments::get_gateway()->get_option( 'manual_capture' ) ? 'automatic' : 'manual', 'is_subscriptions_plugin_active' => WC_Payments::get_gateway()->is_subscriptions_plugin_active(), 'woocommerce_tax_display_cart' => get_option( 'woocommerce_tax_display_cart' ), 'ship_to_billing_address_only' => wc_ship_to_billing_address_only(), 'return_url' => ! $is_pay_for_order ? wc_get_cart_url() : $order->get_checkout_payment_url(), 'blocks_data' => $blocks_data_extractor->get_data(), 'checkout_schema_namespaces' => $blocks_data_extractor->get_checkout_schema_namespaces(), 'optional_fields_status' => self::get_option_fields_status(), ], 'user_session' => null, 'preloaded_requests' => ! $is_pay_for_order ? [ 'cart' => $cart_data, 'checkout' => $checkout_data, ] : [ 'cart' => $cart_data, 'checkout' => [ 'order_id' => $order_id, // This is a workaround for the checkout order error. https://github.com/woocommerce/woocommerce-blocks/blob/04f36065b34977f02079e6c2c8cb955200a783ff/assets/js/blocks/checkout/block.tsx#L81-L83. ], ], 'tracks_user_identity' => WC_Payments::woopay_tracker()->tracks_get_identity(), 'appearance' => $appearance, ]; $woopay_adapted_extensions = new WooPay_Adapted_Extensions(); $request['extension_data'] = $woopay_adapted_extensions->get_extension_data(); if ( ! empty( $email ) ) { // Save email in session to skip TYP verify email and check if // WooPay verified email matches. WC()->customer->set_billing_email( $email ); WC()->customer->save(); $woopay_adapted_extensions->init(); $request['adapted_extensions'] = $woopay_adapted_extensions->get_adapted_extensions_data( $email ); if ( ! is_user_logged_in() && count( $request['adapted_extensions'] ) > 0 ) { $store_user_email_registered = get_user_by( 'email', $email ); if ( $store_user_email_registered ) { $request['email_verified_session_nonce'] = self::create_woopay_nonce( $store_user_email_registered->ID ); } } } return $request; } /** * Recursively map an array. * * @param callable $callback The sanitize_text_field function. * @param array $array The nested array. * * @return array A new appearance array. */ private static function array_map_recursive( $callback, $array ) { $func = function ( $item ) use ( &$func, &$callback ) { return is_array( $item ) ? array_map( $func, $item ) : call_user_func( $callback, $item ); }; return array_map( $func, $array ); } /** * Sanitize a string. * * @param string $item A string. * * @return string The sanitized string. */ private static function sanitize_string( $item ) { return sanitize_text_field( wp_unslash( $item ) ); } /** * Used to initialize woopay session. * * @return void */ public static function ajax_init_woopay() { $is_nonce_valid = check_ajax_referer( 'wcpay_init_woopay_nonce', false, false ); if ( ! $is_nonce_valid ) { wp_send_json_error( __( 'You aren’t authorized to do that.', 'woocommerce-payments' ), 403 ); } $order_id = ! empty( $_POST['order_id'] ) ? absint( wp_unslash( $_POST['order_id'] ) ) : null; $key = ! empty( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : null; $billing_email = ! empty( $_POST['billing_email'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_email'] ) ) : null; $appearance = ! empty( $_POST['appearance'] ) ? self::array_map_recursive( array( __CLASS__, 'sanitize_string' ), $_POST['appearance'] ) : null; // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, Generic.Arrays.DisallowLongArraySyntax.Found $body = self::get_init_session_request( $order_id, $key, $billing_email, null, $appearance ); $body['user_session'] = isset( $_REQUEST['user_session'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['user_session'] ) ) : null; $args = [ 'url' => WooPay_Utilities::get_woopay_rest_url( 'init' ), 'method' => 'POST', 'timeout' => 30, 'body' => wp_json_encode( $body ), 'headers' => [ 'Content-Type' => 'application/json', ], ]; $response = \Automattic\Jetpack\Connection\Client::remote_request( $args, wp_json_encode( $body ) ); if ( is_wp_error( $response ) || ! is_array( $response ) ) { Logger::error( 'HTTP_REQUEST_ERROR ' . var_export( $response, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export // Respond with same message platform would respond with on failure. $response_body_json = wp_json_encode( [ 'result' => 'failure' ] ); } else { $response_body_json = wp_remote_retrieve_body( $response ); } Logger::log( $response_body_json ); wp_send_json( json_decode( $response_body_json ) ); } /** * Used to initialize woopay session on frontend * * @return void */ public static function ajax_get_woopay_session() { $is_nonce_valid = check_ajax_referer( 'woopay_session_nonce', false, false ); if ( ! $is_nonce_valid ) { wp_send_json_error( __( 'You aren’t authorized to do that.', 'woocommerce-payments' ), 403 ); } $blog_id = Jetpack_Options::get_option( 'id' ); if ( empty( $blog_id ) ) { wp_send_json_error( __( 'Could not determine the blog ID.', 'woocommerce-payments' ), 503 ); } wp_send_json( self::get_frontend_init_session_request() ); } /** * Save the blocks checkout phone number in session. * * @return void */ public static function ajax_set_woopay_phone_number() { $is_nonce_valid = check_ajax_referer( 'woopay_session_nonce', false, false ); if ( ! $is_nonce_valid ) { wp_send_json_error( __( 'You aren’t authorized to do that.', 'woocommerce-payments' ), 403 ); } if ( ! ( isset( WC()->session ) && WC()->session->has_session() ) ) { WC()->session->set_customer_session_cookie( true ); } if ( ! empty( $_POST['empty'] ) && filter_var( wp_unslash( $_POST['empty'] ), FILTER_VALIDATE_BOOLEAN ) ) { WC()->session->__unset( self::WOOPAY_SESSION_KEY ); wp_send_json_success(); return; } $data = [ 'save_user_in_woopay' => filter_var( wp_unslash( $_POST['save_user_in_woopay'] ), FILTER_VALIDATE_BOOLEAN ), 'woopay_source_url' => wc_clean( wp_unslash( $_POST['woopay_source_url'] ) ), 'woopay_is_blocks' => filter_var( wp_unslash( $_POST['save_user_in_woopay'] ), FILTER_VALIDATE_BOOLEAN ), 'woopay_viewport' => wc_clean( wp_unslash( $_POST['woopay_viewport'] ) ), 'woopay_user_phone_field' => [ 'full' => wc_clean( wp_unslash( $_POST['woopay_user_phone_field']['full'] ) ), ], ]; WC()->session->set( self::WOOPAY_SESSION_KEY, $data ); wp_send_json_success(); } /** * Used to initialize woopay session on frontend * * @return void */ public static function ajax_get_woopay_minimum_session_data() { $is_nonce_valid = check_ajax_referer( 'woopay_session_nonce', false, false ); if ( ! $is_nonce_valid ) { wp_send_json_error( __( 'You aren’t authorized to do that.', 'woocommerce-payments' ), 403 ); } $blog_id = Jetpack_Options::get_option( 'id' ); if ( empty( $blog_id ) ) { wp_send_json_error( __( 'Could not determine the blog ID.', 'woocommerce-payments' ), 503 ); } wp_send_json( self::get_woopay_minimum_session_data() ); } /** * Return WooPay minimum session data. * * @return array Array of minimum session data used by WooPay or false on failures. */ public static function get_woopay_minimum_session_data() { if ( ! extension_loaded( 'openssl' ) || ! function_exists( 'openssl_encrypt' ) ) { return []; } $blog_id = Jetpack_Options::get_option( 'id' ); if ( empty( $blog_id ) ) { return []; } $data = [ 'wcpay_version' => WCPAY_VERSION_NUMBER, 'blog_id' => $blog_id, 'blog_rest_url' => get_rest_url(), 'blog_checkout_url' => wc_get_checkout_url(), 'session_nonce' => self::create_woopay_nonce( get_current_user_id() ), 'store_api_token' => self::init_store_api_token(), ]; return WooPay_Utilities::encrypt_and_sign_data( $data ); } /** * Returns true if the request that's currently being processed is from WooPay, false * otherwise. * * @return bool True if request is from WooPay. */ public static function is_request_from_woopay(): bool { return isset( $_SERVER['HTTP_USER_AGENT'] ) && 'WooPay' === $_SERVER['HTTP_USER_AGENT']; } /** * Returns true if the request that's currently being processed is signed with the blog token. * * @return bool True if the request signature is valid. */ public static function has_valid_request_signature() { return apply_filters( 'wcpay_woopay_is_signed_with_blog_token', Rest_Authentication::is_signed_with_blog_token() ); } /** * Get the WooPay verified email address from the header. * * @return string|null The WooPay verified email address if it's set. */ private static function get_woopay_verified_email_address() { $has_woopay_verified_email_address = isset( $_SERVER['HTTP_X_WOOPAY_VERIFIED_EMAIL_ADDRESS'] ); return $has_woopay_verified_email_address ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WOOPAY_VERIFIED_EMAIL_ADDRESS'] ) ) : null; } /** * Returns true if WooPay is enabled, false otherwise. * * @return bool True if WooPay is enabled, false otherwise. */ private static function is_woopay_enabled(): bool { // There were previously instances of this function being called too early. While those should be resolved, adding this defensive check as well. if ( ! class_exists( WC_Payments_Features::class ) || ! class_exists( WC_Payments::class ) || is_null( WC_Payments::get_gateway() ) ) { return false; } return WC_Payments_Features::is_woopay_eligible() && 'yes' === WC_Payments::get_gateway()->get_option( 'platform_checkout', 'no' ); } /** * Initializes the WooPay_Store_Api_Token class and returns the Cart token. * * @return string The Cart Token. */ private static function init_store_api_token() { $cart_route = WooPay_Store_Api_Token::init(); return $cart_route->get_cart_token(); } /** * Retrieves the Store API URL. * * @return string */ private static function get_store_api_url() { if ( class_exists( StoreApi::class ) && class_exists( RoutesController::class ) ) { try { $cart = StoreApi::container()->get( RoutesController::class )->get( 'cart' ); $store_api_url = method_exists( $cart, 'get_namespace' ) ? $cart->get_namespace() : 'wc/store'; } catch ( \Exception $e ) { $store_api_url = 'wc/store'; } } return get_rest_url( null, $store_api_url ?? 'wc/store' ); } /** * WooPay requests to the merchant API does not include a cookie, so the token * is always empty. This function creates a nonce that can be used without * a cookie. * * @param int $uid The uid to be used for the nonce. Most likely the user ID. * @return false|string */ private static function create_woopay_nonce( int $uid ) { $action = 'wc_store_api'; $token = ''; $i = wp_nonce_tick( $action ); return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 ); } /** * Gets the custom message from the settings and replaces the placeholders with the correct links. * * @return string The custom message with the placeholders replaced. */ private static function get_formatted_custom_terms() { $custom_message = WC_Payments::get_gateway()->get_option( 'platform_checkout_custom_message' ); $terms_value = wc_terms_and_conditions_page_id() ? '<a href="' . get_permalink( wc_terms_and_conditions_page_id() ) . '">' . __( 'Terms of Service', 'woocommerce-payments' ) . '</a>' : __( 'Terms of Service', 'woocommerce-payments' ); $privacy_policy_value = wc_privacy_policy_page_id() ? '<a href="' . get_permalink( wc_privacy_policy_page_id() ) . '">' . __( 'Privacy Policy', 'woocommerce-payments' ) . '</a>' : __( 'Privacy Policy', 'woocommerce-payments' ); $replacement_map = [ '[terms_of_service_link]' => $terms_value, '[terms]' => $terms_value, '[privacy_policy_link]' => $privacy_policy_value, '[privacy_policy]' => $privacy_policy_value, ]; return str_replace( array_keys( $replacement_map ), array_values( $replacement_map ), $custom_message ); } /** * Returns the status of checkout optional/required address fields. * * @return array The status of the checkout fields. */ private static function get_option_fields_status() { // Shortcode checkout options. $company = get_option( 'woocommerce_checkout_company_field', 'optional' ); $address_2 = get_option( 'woocommerce_checkout_address_2_field', 'optional' ); $phone = get_option( 'woocommerce_checkout_phone_field', 'required' ); $has_terms_and_condition_page = ! empty( get_option( 'woocommerce_terms_page_id', null ) ); $terms_and_conditions = wp_kses_post( wc_replace_policy_page_link_placeholders( wc_get_terms_and_conditions_checkbox_text() ) ); $has_privacy_policy_page = ! empty( get_option( 'wp_page_for_privacy_policy', null ) ); $custom_below_place_order_button_text = self::get_formatted_custom_terms(); $below_place_order_button_text = $custom_below_place_order_button_text; $show_terms_checkbox = false; // Blocks checkout options. To get the blocks checkout options, we need // to parse the checkout page content because the options are stored // in the blocks HTML as a JSON. $checkout_page_id = get_option( 'woocommerce_checkout_page_id' ); $checkout_page = get_post( $checkout_page_id ); /* * Will show the terms checkbox if the terms page is set. * Will show the checkbox even when the text is loaded from the custom field or the policy page field. */ if ( $has_terms_and_condition_page && $terms_and_conditions ) { $show_terms_checkbox = true; if ( ! $below_place_order_button_text ) { $below_place_order_button_text = $terms_and_conditions; } } if ( ! $below_place_order_button_text && $has_privacy_policy_page ) { $show_terms_checkbox = false; $below_place_order_button_text = wp_kses_post( wc_replace_policy_page_link_placeholders( wc_get_privacy_policy_text( 'checkout' ) ) ); } if ( empty( $checkout_page ) ) { return [ 'company' => $company, 'address_2' => $address_2, 'phone' => $phone, 'terms_checkbox' => $show_terms_checkbox, 'custom_terms' => $below_place_order_button_text, ]; } $checkout_page_blocks = parse_blocks( $checkout_page->post_content ); $checkout_block_index = array_search( 'woocommerce/checkout', array_column( $checkout_page_blocks, 'blockName' ), true ); // If we can find the index, it means the merchant checkout page is using blocks checkout. if ( false !== $checkout_block_index ) { $below_place_order_button_text = $custom_below_place_order_button_text; $company = 'optional'; $address_2 = 'optional'; $phone = 'optional'; if ( ! empty( $checkout_page_blocks[ $checkout_block_index ]['attrs'] ) ) { $checkout_block_attrs = $checkout_page_blocks[ $checkout_block_index ]['attrs']; if ( ! empty( $checkout_block_attrs['requireCompanyField'] ) ) { $company = 'required'; } if ( ! empty( $checkout_block_attrs['requirePhoneField'] ) ) { $phone = 'required'; } // showCompanyField is undefined by default. if ( empty( $checkout_block_attrs['showCompanyField'] ) ) { $company = 'hidden'; } if ( isset( $checkout_block_attrs['showApartmentField'] ) && false === $checkout_block_attrs['showApartmentField'] ) { $address_2 = 'hidden'; } if ( isset( $checkout_block_attrs['showPhoneField'] ) && false === $checkout_block_attrs['showPhoneField'] ) { $phone = 'hidden'; } } $fields_block = self::get_inner_block( $checkout_page_blocks[ $checkout_block_index ], 'woocommerce/checkout-fields-block' ); $terms_block = self::get_inner_block( $fields_block, 'woocommerce/checkout-terms-block' ); $show_terms_checkbox = false; $below_place_order_button_text = ''; if ( $terms_block ) { $show_terms_checkbox = isset( $terms_block['attrs']['checkbox'] ) && $terms_block['attrs']['checkbox']; $below_place_order_button_text = self::get_blocks_terms_and_conditions_text( $terms_block, $show_terms_checkbox ); } } return [ 'company' => $company, 'address_2' => $address_2, 'phone' => $phone, 'terms_checkbox' => $show_terms_checkbox, 'custom_terms' => $below_place_order_button_text, ]; } /** * Gets the blocks terms and conditions text. * * @param array $terms_block the terms block. * @param bool $show_terms_checkbox whether the terms checkbox is shown. * @return string */ private static function get_blocks_terms_and_conditions_text( $terms_block, $show_terms_checkbox ) { if ( isset( $terms_block['attrs']['text'] ) && ! empty( $terms_block['attrs']['text'] ) ) { return $terms_block['attrs']['text']; } $privacy_page_link = get_privacy_policy_url(); $privacy_page_link = $privacy_page_link ? '<a href="' . $privacy_page_link . '" target="_blank">' . __( 'Privacy Policy', 'woocommerce-payments' ) . '</a>' : __( 'Privacy Policy', 'woocommerce-payments' ); $terms_page_id = wc_terms_and_conditions_page_id(); $terms_page_link = ''; if ( $terms_page_id ) { $terms_page_link = get_permalink( $terms_page_id ); } $terms_page_link = $terms_page_link ? '<a href="' . $terms_page_link . '" target="_blank">' . __( 'Terms and Conditions', 'woocommerce-payments' ) . '</a>' : __( 'Terms and Conditions', 'woocommerce-payments' ); if ( $show_terms_checkbox ) { return sprintf( /* translators: %1$s terms page link, %2$s privacy page link. */ __( 'You must accept our %1$s and %2$s to continue with your purchase.', 'woocommerce-payments' ), $terms_page_link, $privacy_page_link ); } return sprintf( /* translators: %1$s terms page link, %2$s privacy page link. */ __( 'By proceeding with your purchase you agree to our %1$s and %2$s', 'woocommerce-payments' ), $terms_page_link, $privacy_page_link ); } /** * Searches for an inner block with the given name. * * @param array $current_block A block that contains child blocks. * @param string $inner_block_name The name of a child block. * @return array|null */ private static function get_inner_block( $current_block, $inner_block_name ) { if ( ! isset( $current_block['innerBlocks'] ) ) { return; } $inner_block_index = array_search( $inner_block_name, array_column( $current_block['innerBlocks'], 'blockName' ), true ); if ( ! $inner_block_index || ! isset( $current_block['innerBlocks'][ $inner_block_index ] ) ) { return; } return $current_block['innerBlocks'][ $inner_block_index ]; } /** * Catches and logs errors that occur during WooPay checkout processing. * This is particularly important for third-party plugin compatibility issues * (e.g., calling methods that don't exist on the WooPay SessionHandler). * * This method sets up an error handler that will catch PHP errors and add * them as order notes for debugging purposes. * * @param \WC_Order $order The order being processed. * @return void */ public static function catch_woopay_checkout_errors( $order ) { if ( ! self::is_request_from_woopay() || ! ( $order instanceof \WC_Order ) ) { return; } // Store the order ID so the error handler can access it. self::$checkout_error_order_id = $order->get_id(); if ( self::$is_error_handler_registered ) { return; } // Register shutdown function to catch fatal errors and restore previous handler. register_shutdown_function( function () { $error = error_get_last(); if ( ! $error || ! self::is_request_from_woopay() ) { return; } // Only handle fatal errors. if ( ! in_array( $error['type'], [ E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR ], true ) ) { return; } // Log the fatal error. Logger::error( sprintf( 'WooPay checkout fatal error: %s in %s on line %d', $error['message'], $error['file'], $error['line'] ) ); // Add order note with the error details. if ( self::$checkout_error_order_id ) { $woopay_order = wc_get_order( self::$checkout_error_order_id ); if ( $woopay_order ) { // Extract only the first line of the error message. $error_first_line = strtok( $error['message'], "\n" ); $note = sprintf( /* translators: %s: error message */ __( 'WooPay checkout encountered a fatal error: %s', 'woocommerce-payments' ), esc_html( $error_first_line ) ); $woopay_order->add_order_note( $note ); } } } ); self::$is_error_handler_registered = true; } } includes/woopay/class-woopay-scheduler.php 0000777 00000017113 15251156605 0015016 0 ustar 00 <?php /** * Class WooPay_Scheduler. * * @package WooCommerce\Payments */ namespace WCPay\WooPay; use WC_Payments_API_Client; use WCPay\Logger; /** * This class adds a cron job that runs daily to check if is there any extensions incompatible with WooPay active, * if one is found WooPay is disabled, also updates the list of extensions adapted to work with WooPay and the * available countries. */ class WooPay_Scheduler { const INVALID_EXTENSIONS_FOUND_OPTION_NAME = 'woopay_invalid_extension_found'; const INCOMPATIBLE_EXTENSIONS_LIST_OPTION_NAME = 'woopay_incompatible_extensions'; const ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME = 'woopay_enabled_adapted_extensions'; const ADAPTED_EXTENSIONS_LIST_OPTION_NAME = 'woopay_adapted_extensions'; /** * WC_Payments_API_Client instance. * * @var WC_Payments_API_Client */ private $payments_api_client; /** * Constructor. * * @param WC_Payments_API_Client $payments_api_client The Payments API Client. * @return void */ public function __construct( WC_Payments_API_Client $payments_api_client ) { $this->payments_api_client = $payments_api_client; } /** * Init the hooks. */ public function init() { add_action( 'init', [ $this, 'schedule' ] ); add_action( 'validate_woopay_compatibility', [ $this, 'update_compatibility_and_maybe_show_incompatibility_warning' ] ); add_action( 'activated_plugin', [ $this, 'show_warning_when_incompatible_extension_is_enabled' ] ); add_action( 'deactivated_plugin', [ $this, 'hide_warning_when_incompatible_extension_is_disabled' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ $this, 'remove_legacy_schedule_action_name_on_update' ] ); register_deactivation_hook( WCPAY_PLUGIN_FILE, [ $this, 'remove_scheduler' ] ); } /** * Disables the scheduler when the plugin is disabled. */ public function remove_scheduler() { wp_clear_scheduled_hook( 'validate_woopay_compatibility' ); } /** * Starts the cron job. */ public function schedule() { if ( ! wp_next_scheduled( 'validate_woopay_compatibility' ) ) { wp_schedule_event( time(), 'daily', 'validate_woopay_compatibility' ); } } /** * Remove legacy action name on WCPay update. */ public function remove_legacy_schedule_action_name_on_update() { if ( wp_next_scheduled( 'validate_incompatible_extensions' ) ) { wp_clear_scheduled_hook( 'validate_incompatible_extensions' ); } } /** * Updates the compability list. */ public function update_compatibility_and_maybe_show_incompatibility_warning() { try { $compatibility = $this->payments_api_client->get_woopay_compatibility(); $incompatible_extensions = isset( $compatibility['incompatible_extensions'] ) ? $compatibility['incompatible_extensions'] : []; $adapted_extensions = isset( $compatibility['adapted_extensions'] ) ? $compatibility['adapted_extensions'] : []; $available_countries = isset( $compatibility['available_countries'] ) ? $compatibility['available_countries'] : []; $active_plugins = get_option( 'active_plugins', [] ); update_option( self::INCOMPATIBLE_EXTENSIONS_LIST_OPTION_NAME, $incompatible_extensions ); delete_option( self::INVALID_EXTENSIONS_FOUND_OPTION_NAME ); update_option( self::ADAPTED_EXTENSIONS_LIST_OPTION_NAME, $adapted_extensions ); delete_option( self::ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME ); if ( ! empty( $active_plugins ) && is_array( $active_plugins ) ) { if ( count( $this->get_extensions_in_list( $active_plugins, $incompatible_extensions ) ) > 0 ) { update_option( self::INVALID_EXTENSIONS_FOUND_OPTION_NAME, true ); } } $this->update_enabled_adapted_extensions( $active_plugins, $adapted_extensions ); $this->update_available_countries( $available_countries ); } catch ( \Exception $e ) { Logger::error( 'Failed to decode WooPay incompatible extensions list. ' . $e ); } } /** * Update the enable adapted extensions list. * * @param array $active_plugins The active plugins. * @param array $adapted_extensions The adapted extensions list. */ public function update_enabled_adapted_extensions( $active_plugins, $adapted_extensions ) { $enabled_adapted_extensions = $this->get_extensions_in_list( $active_plugins, $adapted_extensions ); if ( count( $enabled_adapted_extensions ) > 0 ) { update_option( '_wcpay_feature_woopay_first_party_auth', 0 ); } else { update_option( '_wcpay_feature_woopay_first_party_auth', 1 ); } update_option( self::ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME, $enabled_adapted_extensions ); } /** * Update the available countries list. * * @param array $available_countries The available countries list. */ public function update_available_countries( $available_countries ) { try { if ( is_array( $available_countries ) ) { update_option( WooPay_Utilities::AVAILABLE_COUNTRIES_OPTION_NAME, wp_json_encode( $available_countries ) ); } } catch ( \Exception $e ) { Logger::error( 'Failed to decode WooPay available countries. ' . $e ); } } /** * Adds a warning to the WC Payments settings page. * * @param string $plugin The plugin being enabled. */ public function show_warning_when_incompatible_extension_is_enabled( $plugin ) { $incompatible_extensions = get_option( self::INCOMPATIBLE_EXTENSIONS_LIST_OPTION_NAME, [] ); $adapted_extensions = get_option( self::ADAPTED_EXTENSIONS_LIST_OPTION_NAME, [] ); $plugin = $this->format_extension_name( $plugin ); $active_plugins = get_option( 'active_plugins', [] ); if ( count( $this->get_extensions_in_list( [ $plugin ], $incompatible_extensions ) ) > 0 ) { update_option( self::INVALID_EXTENSIONS_FOUND_OPTION_NAME, true ); } $this->update_enabled_adapted_extensions( $active_plugins, $adapted_extensions ); } /** * Removes the warning when the last incompatible extension is removed. * * @param string $plugin_being_deactivated The plugin name. */ public function hide_warning_when_incompatible_extension_is_disabled( $plugin_being_deactivated ) { $incompatible_extensions = get_option( self::INCOMPATIBLE_EXTENSIONS_LIST_OPTION_NAME, [] ); $adapted_extensions = get_option( self::ADAPTED_EXTENSIONS_LIST_OPTION_NAME, [] ); $active_plugins = get_option( 'active_plugins', [] ); // Needs to remove the plugin being deactivated because WordPress only updates the list after this hook runs. $active_plugins = array_diff( $active_plugins, [ $plugin_being_deactivated ] ); // Only deactivates the warning if there are no other incompatible extensions. if ( count( $this->get_extensions_in_list( $active_plugins, $incompatible_extensions ) ) === 0 ) { delete_option( self::INVALID_EXTENSIONS_FOUND_OPTION_NAME ); } $this->update_enabled_adapted_extensions( $active_plugins, $adapted_extensions ); } /** * Get the list of extensions in a list. * * @param mixed $active_plugins list of active plugins. * @param mixed $extensions list of extensions to find in active plugins. * * @return array */ public function get_extensions_in_list( $active_plugins, $extensions ) { $plugins_in_list = []; foreach ( $active_plugins as $plugin ) { $plugin = $this->format_extension_name( $plugin ); if ( in_array( $plugin, $extensions, true ) ) { $plugins_in_list[] = $plugin; } } return $plugins_in_list; } /** * Removes the folder and file extension from the plugin name. * * @param string $plugin the plugin main file name. * @return string the plugin name. */ private function format_extension_name( $plugin ) { $plugin = explode( '/', $plugin ); $plugin = end( $plugin ); return str_replace( '.php', '', $plugin ); } } includes/woopay/class-woopay-store-api-token.php 0000777 00000003202 15251156605 0016053 0 ustar 00 <?php /** * Class WooPay_Store_Api_Token * * @package WCPay\Platform_Checkout */ namespace WCPay\Platform_Checkout; use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractCartRoute; if ( class_exists( AbstractCartRoute::class ) ) { /** * This class is used to get the cart token from the cart route. */ class WooPay_Store_Api_Token extends AbstractCartRoute { /** * Helper method to get the instance of the class. * * @return WooPay_Store_Api_Token The instance of the class. */ public static function init() { $formatters = new \Automattic\WooCommerce\StoreApi\Formatters(); $extend_schema = new \Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema( $formatters ); $schema_controller = new \Automattic\WooCommerce\StoreApi\SchemaController( $extend_schema ); return new self( $schema_controller, $schema_controller->get( 'cart' ) ); } /** * Get the path of this REST route. * * @throws \Exception Throws exception because this method is not meant to be implemented in this utility class. */ public function get_path() { throw new \Exception( 'Not implemented' ); } /** * Get arguments for this REST route. * * @throws \Exception Throws exception because this method is not meant to be implemented in this utility class. */ public function get_args() { throw new \Exception( 'Not implemented' ); } //phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod /** * This function is used to get the cart token from the cart route. * * @return string The cart token. */ public function get_cart_token() { return parent::get_cart_token(); } } } includes/woopay/class-woopay-utilities.php 0000777 00000027042 15251156605 0015055 0 ustar 00 <?php /** * WooPay * * @package WCPay\WooPay */ namespace WCPay\WooPay; use WC_Payments_Features; use WC_Payments_Subscriptions_Utilities; use WCPay\Logger; use WC_Geolocation; use WC_Payments; use Jetpack_Options; /** * WooPay */ class WooPay_Utilities { use WC_Payments_Subscriptions_Utilities; const AVAILABLE_COUNTRIES_OPTION_NAME = 'woocommerce_woocommerce_payments_woopay_available_countries'; const AVAILABLE_COUNTRIES_DEFAULT = '["US"]'; const DEFAULT_WOOPAY_URL = 'https://pay.woo.com'; /** * Check various conditions to determine if we should enable woopay. * * @param \WC_Payment_Gateway_WCPay $gateway Gateway instance. * @return boolean */ public function should_enable_woopay( $gateway ) { $is_woopay_eligible = WC_Payments_Features::is_woopay_eligible(); // Feature flag. $is_woopay_enabled = 'yes' === $gateway->get_option( 'platform_checkout', 'no' ); return $is_woopay_eligible && $is_woopay_enabled; } /** * Check conditions to determine if WooPay should be enabled for guest checkout. * * @return bool True if WooPay should be enabled, false otherwise. */ public function should_enable_woopay_on_guest_checkout(): bool { if ( ! is_user_logged_in() ) { // If there's a subscription product in the cart and the customer isn't logged in we // should not enable WooPay since that situation is currently not supported. // Note that this is mirrored in the WC_Payments_WooPay_Button_Handler class. if ( class_exists( 'WC_Subscriptions_Cart' ) && \WC_Subscriptions_Cart::cart_contains_subscription() ) { return false; } // If guest checkout is disabled and the customer isn't logged in we should not enable // WooPay scripts since that situations is currently not supported. // Note that this is mirrored in the WC_Payments_WooPay_Button_Handler class. if ( ! $this->is_guest_checkout_enabled() ) { return false; } } return true; } /** * Check conditions to determine if woopay express checkout is enabled. * * @return boolean */ public function is_woopay_express_checkout_enabled() { return WC_Payments_Features::is_woopay_express_checkout_enabled() && $this->is_country_available( WC_Payments::get_gateway() ); // Feature flag. } /** * Check conditions to determine if woopay first party auth is enabled. * * @return bool */ public function is_woopay_first_party_auth_enabled() { return WC_Payments_Features::is_woopay_first_party_auth_enabled() && $this->is_country_available( WC_Payments::get_gateway() ); // Feature flag. } /** * Determines if the WooPay email input hooks should be enabled. * * This function doesn't affect the appearance of the email input, * only whether or not the email exists check or auto-redirection should be enabled. * * @return bool */ public function is_woopay_email_input_enabled() { return apply_filters( 'wcpay_is_woopay_email_input_enabled', true ); } /** * Generates a hash based on the store's blog token, merchant ID, and the time step window. * * @return string */ public function get_woopay_request_signature() { $store_blog_token = \Jetpack_Options::get_option( 'blog_token' ); $time_step_window = floor( time() / 30 ); return hash_hmac( 'sha512', \Jetpack_Options::get_option( 'id' ) . $time_step_window, $store_blog_token ); } /** * Check session to determine if we should create a platform customer. * * @return boolean */ public function should_save_platform_customer() { $session_data = []; if ( isset( WC()->session ) && method_exists( WC()->session, 'has_session' ) && WC()->session->has_session() ) { $session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY ); } $save_user_in_woopay_post = isset( $_POST['save_user_in_woopay'] ) && filter_var( wp_unslash( $_POST['save_user_in_woopay'] ), FILTER_VALIDATE_BOOLEAN ); // phpcs:ignore WordPress.Security.NonceVerification $save_user_in_woopay_session = isset( $session_data['save_user_in_woopay'] ) && filter_var( $session_data['save_user_in_woopay'], FILTER_VALIDATE_BOOLEAN ); return $save_user_in_woopay_post || $save_user_in_woopay_session; } /** * Get if WooPay is available on the user country. * * @return boolean */ public function is_country_available() { if ( WC_Payments::mode()->is_test() ) { return true; } $location_data = WC_Geolocation::geolocate_ip(); $available_countries = self::get_persisted_available_countries(); return in_array( $location_data['country'], $available_countries, true ); } /** * Sanitizes an intent ID by stripping everything by underscores, characters and digits. * * @param string $intent_id ID of the intent. * @return string Sanitized value. */ public static function sanitize_intent_id( string $intent_id ) { return preg_replace( '/[^\w_]+/', '', $intent_id ); } /** * Get if WooPay is available on the store country. * * @return boolean */ public static function is_store_country_available() { $store_base_location = wc_get_base_location(); if ( empty( $store_base_location['country'] ) ) { return false; } $available_countries = self::get_persisted_available_countries(); return in_array( $store_base_location['country'], $available_countries, true ); } /** * Get phone number for creating woopay customer. * * @return mixed|string */ public function get_woopay_phone() { $session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY ); if ( ! empty( $_POST['woopay_user_phone_field']['full'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return wc_clean( wp_unslash( $_POST['woopay_user_phone_field']['full'] ) ); // phpcs:ignore WordPress.Security.NonceVerification } elseif ( ! empty( $session_data['woopay_user_phone_field']['full'] ) ) { return $session_data['woopay_user_phone_field']['full']; } return ''; } /** * Get the url marketing where the user have chosen marketing options. * * @return mixed|string */ public function get_woopay_source_url() { $session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY ); if ( ! empty( $_POST['woopay_source_url'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return wc_clean( wp_unslash( $_POST['woopay_source_url'] ) ); // phpcs:ignore WordPress.Security.NonceVerification } elseif ( ! empty( $session_data['woopay_source_url'] ) ) { return $session_data['woopay_source_url']; } return ''; } /** * Get if the request comes from blocks checkout. * * @return boolean */ public function get_woopay_is_blocks() { $session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY ); $woopay_is_blocks_post = isset( $_POST['woopay_is_blocks'] ) && filter_var( wp_unslash( $_POST['woopay_is_blocks'] ), FILTER_VALIDATE_BOOLEAN ); // phpcs:ignore WordPress.Security.NonceVerification $woopay_is_blocks_session = isset( $session_data['woopay_is_blocks'] ) && filter_var( $session_data['woopay_is_blocks'], FILTER_VALIDATE_BOOLEAN ); return $woopay_is_blocks_post || $woopay_is_blocks_session; } /** * Get the user viewport. * * @return mixed|string */ public function get_woopay_viewport() { $session_data = WC()->session->get( WooPay_Session::WOOPAY_SESSION_KEY ); if ( ! empty( $_POST['woopay_viewport'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return wc_clean( wp_unslash( $_POST['woopay_viewport'] ) ); // phpcs:ignore WordPress.Security.NonceVerification } elseif ( ! empty( $session_data['woopay_viewport'] ) ) { return $session_data['woopay_viewport']; } return ''; } /** * Returns true if guest checkout is enabled, false otherwise. * * @return bool True if guest checkout is enabled, false otherwise. */ public function is_guest_checkout_enabled(): bool { return 'yes' === get_option( 'woocommerce_enable_guest_checkout', 'no' ); } /** * Builds the WooPay rest url for a given endpoint * * @param string $endpoint the end point. * @return string the endpoint full url. */ public static function get_woopay_rest_url( $endpoint ) { return self::get_woopay_url() . '/wp-json/platform-checkout/v1/' . $endpoint; } /** * Returns the WooPay url. * * @return string the WooPay url. */ public static function get_woopay_url() { return defined( 'PLATFORM_CHECKOUT_HOST' ) ? PLATFORM_CHECKOUT_HOST : self::DEFAULT_WOOPAY_URL; } /** * Get the store blog token. * * @return mixed|string the store blog token. */ public static function get_store_blog_token() { if ( self::get_woopay_url() === self::DEFAULT_WOOPAY_URL ) { // Using WooPay production: Use the blog token secret from the store blog. return Jetpack_Options::get_option( 'blog_token' ); } elseif ( apply_filters( 'wcpay_woopay_use_blog_token', false ) ) { // Requested to use the blog token secret from the store blog. return Jetpack_Options::get_option( 'blog_token' ); } elseif ( defined( 'DEV_BLOG_TOKEN_SECRET' ) ) { // Has a defined dev blog token secret: Use it. return DEV_BLOG_TOKEN_SECRET; } else { Logger::log( __( 'WooPay blog_token is currently misconfigured.', 'woocommerce-payments' ) ); return ''; } } /** * Return an array with encrypted and signed data. * * @param array $data The data to be encrypted and signed. * @return array The encrypted and signed data. */ public static function encrypt_and_sign_data( $data ) { $store_blog_token = self::get_store_blog_token(); if ( empty( $store_blog_token ) ) { return []; } $message = wp_json_encode( $data ); // Generate an initialization vector (IV) for encryption. $iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( 'aes-256-cbc' ) ); // Encrypt the JSON session. $session_encrypted = openssl_encrypt( $message, 'aes-256-cbc', $store_blog_token, OPENSSL_RAW_DATA, $iv ); // Create an HMAC hash for data integrity. $hash = hash_hmac( 'sha256', $session_encrypted, $store_blog_token ); $data = [ 'session' => $session_encrypted, 'iv' => $iv, 'hash' => $hash, ]; return [ 'blog_id' => Jetpack_Options::get_option( 'id' ), 'data' => array_map( 'base64_encode', $data ), ]; } /** * Decode encrypted and signed data and return it. * * @param array $data The session, iv, and hash data for the encryption. * @return mixed The decoded data. */ public static function decrypt_signed_data( $data ) { $store_blog_token = self::get_store_blog_token(); if ( empty( $store_blog_token ) ) { return null; } // Decode the data. $decoded_data_request = array_map( 'base64_decode', $data ); // Verify the HMAC hash before decryption to ensure data integrity. $computed_hash = hash_hmac( 'sha256', $decoded_data_request['iv'] . $decoded_data_request['data'], $store_blog_token ); // If the hashes don't match, the message may have been tampered with. if ( ! hash_equals( $computed_hash, $decoded_data_request['hash'] ) ) { return null; } // Decipher the data using the blog token and the IV. $decrypted_data = openssl_decrypt( $decoded_data_request['data'], 'aes-256-cbc', $store_blog_token, OPENSSL_RAW_DATA, $decoded_data_request['iv'] ); if ( false === $decrypted_data ) { return null; } $decrypted_data = json_decode( $decrypted_data, true ); return $decrypted_data; } /** * Get the persisted available countries. * * @return array */ private static function get_persisted_available_countries() { $available_countries = json_decode( get_option( self::AVAILABLE_COUNTRIES_OPTION_NAME, self::AVAILABLE_COUNTRIES_DEFAULT ), true ); if ( ! is_array( $available_countries ) ) { return json_decode( self::AVAILABLE_COUNTRIES_DEFAULT ); } return $available_countries; } } includes/woopay/class-woopay-adapted-extensions.php 0000777 00000020232 15251156605 0016633 0 ustar 00 <?php /** * WooPay * * @package WCPay\WooPay */ namespace WCPay\WooPay; use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry; use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface; /** * WooPay */ class WooPay_Adapted_Extensions extends IntegrationRegistry { const POINTS_AND_REWARDS_PLUGIN = 'woocommerce-points-and-rewards'; const POINTS_AND_REWARDS_API = 'points-and-rewards'; const GIFT_CARDS_API = 'woocommerce-gift-cards'; const GIFT_CARDS_BLOCKS = 'wc-gift-cards-blocks'; /** * Initializa WC Blocks regitered integrations. */ public function init() { do_action( 'woocommerce_blocks_checkout_block_registration', $this ); } /** * Get WooPay adapted extensions settings and extra data needed on WooPay. * * @param string $email The user email the data will be loaded. * * @return array The extensions script data. */ public function get_adapted_extensions_data( $email ) { $enabled_adapted_extensions = get_option( WooPay_Scheduler::ENABLED_ADAPTED_EXTENSIONS_OPTION_NAME, [] ); if ( ( is_countable( $enabled_adapted_extensions ) ? count( $enabled_adapted_extensions ) : 0 ) === 0 ) { return []; } $extension_settings = []; $user = wp_get_current_user(); if ( ! is_user_logged_in() ) { // If the user is a guest and has an account on the merchant site, load data // from there to check if we need to verify their email on WooPay later. $user_by_email = get_user_by( 'email', $email ); if ( false !== $user_by_email ) { $user = $user_by_email; } } // Points and Rewards. if ( in_array( self::POINTS_AND_REWARDS_PLUGIN, $enabled_adapted_extensions, true ) ) { $points_and_rewards_data = self::get_points_and_rewards_data( $user ); if ( null !== $points_and_rewards_data ) { $extension_settings[ self::POINTS_AND_REWARDS_API ] = $points_and_rewards_data; } } if ( in_array( self::GIFT_CARDS_API, $enabled_adapted_extensions, true ) ) { $gift_cards_data = self::get_gift_cards_data( $user ); if ( null !== $gift_cards_data ) { $extension_settings[ self::GIFT_CARDS_API ] = $gift_cards_data; } } return $extension_settings; } /** * Get Points and Rewards settings for WooPay. * * @param \WP_User $user The user the data will be loaded. * * @return array|null The Points and Rewards script data if installed. */ public function get_points_and_rewards_data( $user ) { if ( empty( $this->registered_integrations[ self::POINTS_AND_REWARDS_API ] ) || ! class_exists( 'WC_Points_Rewards_Manager' ) || ! method_exists( 'WC_Points_Rewards_Manager', 'get_users_points' ) ) { return null; } $points_and_rewards_script_data = $this->registered_integrations[ self::POINTS_AND_REWARDS_API ]->get_script_data(); list( $points, $monetary_value ) = explode( ':', get_option( 'wc_points_rewards_redeem_points_ratio', '' ) ); $points = floatval( $points ); $monetary_value = floatval( $monetary_value ); $points_and_rewards_script_data['points_ratio'] = [ 'points' => $points, 'monetary_value' => $monetary_value, ]; /** * Check if the user has points to show the verify email alert. */ $available_points_for_user = \WC_Points_Rewards_Manager::get_users_points( $user->ID ); if ( $available_points_for_user > 0 && $available_points_for_user > $points_and_rewards_script_data['minimum_points_amount'] ) { // Only ask the user to verify email if they have available points. $points_and_rewards_script_data['should_verify_email'] = ! is_user_logged_in(); $points_and_rewards_script_data['points_available'] = $available_points_for_user; } return $points_and_rewards_script_data; } /** * Get Gift Cards settings for WooPay. * * @param \WP_User $user The user the data will be loaded. * * @return array|null The Gift Cards script data if installed. */ public function get_gift_cards_data( $user ) { if ( empty( $this->registered_integrations[ self::GIFT_CARDS_BLOCKS ] ) || ! function_exists( 'WC_GC' ) || ! property_exists( WC_GC(), 'account' ) || ! method_exists( WC_GC()->account, 'get_active_giftcards' ) ) { return null; } $gift_cards_script_data = $this->registered_integrations[ self::GIFT_CARDS_BLOCKS ]->get_script_data(); $gift_cards_script_data['should_verify_email'] = false; if ( ! is_user_logged_in() ) { // Verify if the user has Gift Card balance to ask them to verify email on WooPay. $gift_cards = WC_GC()->account->get_active_giftcards( $user->ID ); $balance = 0; foreach ( $gift_cards as $giftcard_data ) { $balance += (float) $giftcard_data->get_balance(); } if ( $balance > 0 ) { $gift_cards_script_data['should_verify_email'] = true; } } return $gift_cards_script_data; } /** * The custom data from plugins to be used on WooPay, * it's not an adapted extension because it doesn't * use the email verification integration. * * @return array The custom data. */ public function get_extension_data() { $extension_data = []; if ( defined( 'WOOCOMMERCE_MULTICURRENCY_VERSION' ) ) { $extension_data['woocommerce-multicurrency'] = [ 'currency' => get_woocommerce_currency(), ]; } if ( $this->is_affiliate_for_woocommerce_enabled() && function_exists( 'afwc_get_referrer_id' ) ) { $extension_data['affiliate-for-woocommerce'] = [ 'affiliate-user' => afwc_get_referrer_id(), ]; } if ( $this->is_automate_woo_referrals_enabled() ) { $advocate_id = $this->get_automate_woo_advocate_id_from_cookie(); $extension_data['automatewoo-referrals'] = [ 'advocate_id' => $advocate_id, ]; } return $extension_data; } /** * Update order extension data after finishing * an order on WooPay, this usually is needed * for extensions which uses cookies when an * order is finished. * * @param int $order_id The successful WooPay order. */ public function update_order_extension_data( $order_id ) { if ( ! empty( $_GET['affiliate'] ) && // phpcs:ignore WordPress.Security.NonceVerification $this->is_affiliate_for_woocommerce_enabled() ) { $affiliate_id = (int) wc_clean( wp_unslash( $_GET['affiliate'] ) ); // phpcs:ignore WordPress.Security.NonceVerification if ( class_exists( '\AFWC_API' ) ) { $affiliate_api = \AFWC_API::get_instance(); $affiliate_api->track_conversion( $order_id, $affiliate_id, '', [ 'is_affiliate_eligible' => true ] ); } } } /** * Get WC Blocks registered integrations. * * @param IntegrationInterface $integration An instance of IntegrationInterface. * * @return boolean True means registered successfully. */ public function register( IntegrationInterface $integration ) { $name = $integration->get_name(); if ( self::GIFT_CARDS_BLOCKS === $name || self::POINTS_AND_REWARDS_API === $name ) { $this->registered_integrations[ $name ] = $integration; } return true; } /** * Check if Affiliate for WooCommerce is enabled and * its functions used on WCPay are available. * * @return boolean */ public function is_affiliate_for_woocommerce_enabled() { return defined( 'AFWC_PLUGIN_FILE' ) && function_exists( 'afwc_get_referrer_id' ) && class_exists( 'AFWC_API' ) && method_exists( 'AFWC_API', 'get_instance' ) && method_exists( 'AFWC_API', 'track_conversion' ); } /** * Check if Automate Woo Referrals is enabled and * its functions used on WCPay are available. * * @return boolean */ private function is_automate_woo_referrals_enabled() { return function_exists( 'AW_Referrals' ) && method_exists( AW_Referrals(), 'options' ) && AW_Referrals()->options()->type === 'link' && class_exists( '\AutomateWoo\Referrals\Referral_Manager' ) && method_exists( \AutomateWoo\Referrals\Referral_Manager::class, 'get_advocate_key_from_cookie' ); } /** * Get AutomateWoo advocate id from cookie. * * @return string|null */ private function get_automate_woo_advocate_id_from_cookie() { if ( class_exists( '\AutomateWoo\Referrals\Referral_Manager' ) ) { $advocate_from_key_cookie = \AutomateWoo\Referrals\Referral_Manager::get_advocate_key_from_cookie(); return $advocate_from_key_cookie ? $advocate_from_key_cookie->get_advocate_id() : null; } return null; } } includes/class-wc-payments-webhook-reliability-service.php 0000777 00000015312 15251156605 0020055 0 ustar 00 <?php /** * WC_Payments_Webhook_Reliability_Service class * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\Invalid_Webhook_Data_Exception; use WCPay\Logger; /** * Improve webhook reliability by fetching failed events from the server, * then process them with ActionScheduler */ class WC_Payments_Webhook_Reliability_Service { const CONTINUOUS_FETCH_FLAG_EVENTS_LIST = 'has_more'; const CONTINUOUS_FETCH_FLAG_ACCOUNT_DATA = 'has_more_failed_events'; const WEBHOOK_FETCH_EVENTS_ACTION = 'wcpay_webhook_fetch_events'; const WEBHOOK_PROCESS_EVENT_ACTION = 'wcpay_webhook_process_event'; /** * Client for making requests to the WooCommerce Payments API. * * @var WC_Payments_API_Client */ private $payments_api_client; /** * WC_Payments_Action_Scheduler_Service. * * @var WC_Payments_Action_Scheduler_Service */ private $action_scheduler_service; /** * Webhook Processing Service. * * @var WC_Payments_Webhook_Processing_Service */ private $webhook_processing_service; /** * WC_Payments_Webhook_Reliability_Service constructor. * * @param WC_Payments_API_Client $payments_api_client WooCommerce Payments API client. * @param WC_Payments_Action_Scheduler_Service $action_scheduler_service Wrapper for ActionScheduler service. * @param WC_Payments_Webhook_Processing_Service $webhook_processing_service WC_Payments_Webhook_Processing_Service instance. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Action_Scheduler_Service $action_scheduler_service, WC_Payments_Webhook_Processing_Service $webhook_processing_service ) { $this->payments_api_client = $payments_api_client; $this->action_scheduler_service = $action_scheduler_service; $this->webhook_processing_service = $webhook_processing_service; // Note: Sometimes the `woocommerce_payments_account_refreshed` hook is ran before ActionScheduler is initialized. // In that case, we will not be able to schedule jobs. We will just ignore it. add_action( 'woocommerce_payments_account_refreshed', [ $this, 'maybe_schedule_fetch_events' ] ); add_action( self::WEBHOOK_FETCH_EVENTS_ACTION, [ $this, 'fetch_events_and_schedule_processing_jobs' ] ); add_action( self::WEBHOOK_PROCESS_EVENT_ACTION, [ $this, 'process_event' ] ); } /** * During the account data refresh, check the relevant flag to remaining failed events on the WooCommerce Payments server, * and decide whether scheduling a job to fetch them. * * @param mixed|array $account Account data retrieved from WooCommerce Payments server. * * @return void */ public function maybe_schedule_fetch_events( $account ) { if ( ! is_array( $account ) ) { return; } if ( $account[ self::CONTINUOUS_FETCH_FLAG_ACCOUNT_DATA ] ?? false ) { $this->schedule_fetch_events(); } } /** * Fetch failed events from the WooCommerce Payments server through ActionScheduler. * * @return void */ public function fetch_events_and_schedule_processing_jobs() { try { $payload = $this->payments_api_client->get_failed_webhook_events(); } catch ( API_Exception $e ) { Logger::error( 'Can not fetch failed events from the server. Error:' . $e->getMessage() ); return; } if ( $payload[ self::CONTINUOUS_FETCH_FLAG_EVENTS_LIST ] ?? false ) { $this->schedule_fetch_events(); } // Save the data, and schedule a job for each event. $events = $payload['data'] ?? []; foreach ( $events as $event ) { if ( ! isset( $event['id'] ) ) { Logger::error( 'Event ID does not exist. Event data: ' . var_export( $event, true ) ); // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export continue; } $this->set_event_data( $event ); $this->schedule_process_event( $event['id'] ); } } /** * Process an event through ActionScheduler. * * @param string $event_id Event ID. * * @return void */ public function process_event( string $event_id ) { Logger::info( 'Start processing event: ' . $event_id ); $event_data = $this->get_event_data( $event_id ); $this->delete_event_data( $event_id ); if ( null === $event_data ) { Logger::error( 'Stop processing as no data available for event: ' . $event_id ); return; } try { $this->webhook_processing_service->process( $event_data ); Logger::info( 'Successfully processed event ' . $event_id ); } catch ( Invalid_Webhook_Data_Exception $e ) { Logger::error( 'Failed processing event ' . $event_id . '. Reason: ' . $e->getMessage() ); } } /** * Schedule a job to process an event later. * * @param string $event_id Event ID. * * @return void */ private function schedule_process_event( string $event_id ) { $this->action_scheduler_service->schedule_job( time(), self::WEBHOOK_PROCESS_EVENT_ACTION, [ 'event_id' => $event_id ] ); Logger::info( 'Successfully scheduled a job to process event: ' . $event_id ); } /** * Schedule a job to fetch failed events. * * We will bail if this is called too early and ActionScheduler is not initialized. * * @return void */ private function schedule_fetch_events() { $this->action_scheduler_service->schedule_job( time(), self::WEBHOOK_FETCH_EVENTS_ACTION ); Logger::info( 'Successfully scheduled a job to fetch failed events from the server.' ); } /** * Get the transient name to interact with the storage. * * @param string $event_id Event ID. * * @return string */ private function get_transient_name_for_event_id( string $event_id ): string { // Use md5 to overcome the limit of transient name (172 characters) while Stripe event ID can be up to 255. return 'wcpay_failed_event_' . md5( $event_id ); } /** * Save the event data. * * @param array $event_data Event data. * * @return bool True if the value was set, false otherwise. */ public function set_event_data( array $event_data ) { if ( ! isset( $event_data['id'] ) ) { return false; } return set_transient( $this->get_transient_name_for_event_id( $event_data['id'] ), $event_data, DAY_IN_SECONDS ); } /** * Delete the event data. * * @param string $event_id Event ID. * * @return bool True if the event data is deleted, false otherwise. */ public function delete_event_data( string $event_id ): bool { return delete_transient( $this->get_transient_name_for_event_id( $event_id ) ); } /** * Retrieve the event data. Return null if the data does not exist. * * @param string $event_id Event ID. * * @return ?array */ public function get_event_data( string $event_id ) { $data = get_transient( $this->get_transient_name_for_event_id( $event_id ) ); return false === $data ? null : $data; } } includes/class-experimental-abtest.php 0000777 00000011604 15251156605 0014162 0 ustar 00 <?php /** * A class that interacts with Explat A/B tests. * * This class is experimental. It is a fork of the jetpack-abtest package and * updated for use with ExPlat. These changes are planned to be contributed * back to the upstream Jetpack package. If accepted, this class should then * be superseded by the Jetpack class using Composer. * * This class should not be used externally. * * @package WooCommerce\Payments * @link https://packagist.org/packages/automattic/jetpack-abtest */ namespace WCPay; /** * This class provides an interface to the Explat A/B tests. * * @internal This class is experimental and should not be used externally due to planned breaking changes. */ final class Experimental_Abtest { /** * A variable to hold the tests we fetched, and their variations for the current user. * * @var array */ private $tests = []; /** * ExPlat Anonymous ID. * * @var string */ private $anon_id = null; /** * ExPlat Platform name. * * @var string */ private $platform = 'woocommerce'; /** * Whether trcking consent is given. * * @var bool */ private $consent = false; /** * Constructor. * * @param string $anon_id ExPlat anonymous ID. * @param string $platform ExPlat platform name. * @param bool $consent Whether tracking consent is given. */ public function __construct( string $anon_id, string $platform, bool $consent ) { $this->anon_id = $anon_id; $this->platform = $platform; $this->consent = $consent; } /** * Retrieve the test variation for a provided A/B test. * * @param string $test_name Name of the A/B test. * @return mixed A/B test variation, or null on failure. */ public function get_variation( $test_name ) { // Default to the control variation when users haven't consented to tracking. if ( ! $this->consent ) { return 'control'; } $variation = $this->fetch_variation( $test_name ); // If there was an error retrieving a variation, conceal the error for the consumer. if ( is_wp_error( $variation ) ) { return 'control'; } return $variation; } /** * Fetch and cache the test variation for a provided A/B test from WP.com. * * ExPlat returns a null value when the assigned variation is control or * an assignment has not been set. In these instances, this method returns * a value of "control". * * @param string $test_name Name of the A/B test. * @return string|array|\WP_Error A/B test variation, or error on failure. */ protected function fetch_variation( $test_name ) { // Make sure test name exists. if ( ! $test_name ) { return new \WP_Error( 'test_name_not_provided', 'A/B test name has not been provided.' ); } // Make sure test name is a valid one. if ( ! preg_match( '/^[[:alnum:]_]+$/', $test_name ) ) { return new \WP_Error( 'invalid_test_name', 'Invalid A/B test name.' ); } // Return internal-cached test variations. if ( isset( $this->tests[ $test_name ] ) ) { return $this->tests[ $test_name ]; } // Return external-cached test variations. if ( ! empty( get_transient( 'abtest_variation_' . $test_name ) ) ) { return get_transient( 'abtest_variation_' . $test_name ); } // Make the request to the WP.com API. $response = $this->request_variation( $test_name ); // Bail if there was an error or malformed response. if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) { return new \WP_Error( 'failed_to_fetch_data', 'Unable to fetch the requested data.' ); } // Decode the results. $results = json_decode( $response['body'], true ); // Bail if there were no results or there is no test variation returned. if ( ! is_array( $results ) || empty( $results['variations'] ) ) { return new \WP_Error( 'unexpected_data_format', 'Data was not returned in the expected format.' ); } // Store the variation in our internal cache. $this->tests[ $test_name ] = $results['variations'][ $test_name ]; $variation = $results['variations'][ $test_name ] ?? 'control'; // Store the variation in our external cache. if ( ! empty( $results['ttl'] ) ) { set_transient( 'abtest_variation_' . $test_name, $variation, $results['ttl'] ); } return $variation; } /** * Perform the request for a variation of a provided A/B test from WP.com. * * @param string $test_name Name of the A/B test. * @return array|\WP_Error A/B test variation error on failure. */ protected function request_variation( $test_name ) { $args = [ 'experiment_name' => $test_name, 'anon_id' => $this->anon_id, 'woo_country_code' => get_option( 'woocommerce_default_country' ), ]; $url = add_query_arg( $args, sprintf( // nosemgrep: audit.php.wp.security.xss.query-arg -- constant value is passed in to add_query_arg. 'https://public-api.wordpress.com/wpcom/v2/experiments/0.1.0/assignments/%s', $this->platform ) ); $get = wp_remote_get( $url ); return $get; } } includes/class-payment-information.php 0000777 00000037245 15251156605 0014216 0 ustar 00 <?php /** * Class Payment_Information * * @package WooCommerce\Payments */ namespace WCPay; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Constants\Payment_Method; use WCPay\Constants\Payment_Type; use WCPay\Constants\Payment_Initiated_By; use WCPay\Constants\Payment_Capture_Type; use WCPay\Exceptions\Invalid_Payment_Method_Exception; /** * Mostly a wrapper containing information on a single payment. */ class Payment_Information { /** * Key used to indicate that an error occurred during the payment method creation in the client. * * @type string */ const PAYMENT_METHOD_ERROR = 'woocommerce_payments_payment_method_error'; /** * The ID of the payment method used for this payment. * * @var string */ private $payment_method; /** * The order object. * * @var ?\WC_Order */ private $order; /** * The payment token used for this payment. * * @var ?\WC_Payment_Token */ private $token; /** * The CVC confirmation used for this payment. * * @var string */ private $cvc_confirmation; /** * Indicates whether the payment is merchant-initiated (true) or customer-initiated (false). * * @var ?Payment_Initiated_By */ private $payment_initiated_by; /** * Indicates whether the payment will be only authorized (true) or captured immediately (false). * * @var ?Payment_Capture_Type */ private $manual_capture; /** * The type of the payment. `single`, `recurring`, etc. * * @var Payment_Type */ private $payment_type; /** * Indicates whether the payment method should be saved to the store. * * @var bool */ private $save_payment_method_to_store = false; /** * Indicates whether the payment method should be saved to the platform. * * @var bool */ private $save_payment_method_to_platform = false; /** * Indicates whether user is changing payment method for subscriptions order. * * @var bool */ private $is_changing_payment_method_for_subscription = false; /** * The attached fingerprint. * * @var string */ private $fingerprint = ''; /** * The Stripe ID of the payment method used for this payment. * * @var string */ private $payment_method_stripe_id; /** * The WCPay Customer ID that owns the payment token. * * @var string */ private $customer_id; /** * Will be set if there was an error during setup. * * @var ?\WP_Error */ private $error = null; /** * Payment information constructor. * * @param string $payment_method The ID of the payment method used for this payment. * @param \WC_Order $order The order object. * @param Payment_Type $payment_type The type of the payment. * @param \WC_Payment_Token $token The payment token used for this payment. * @param Payment_Initiated_By $payment_initiated_by Indicates whether the payment is merchant-initiated or customer-initiated. * @param Payment_Capture_Type $manual_capture Indicates whether the payment will be only authorized or captured immediately. * @param string $cvc_confirmation The CVC confirmation for this payment method. * @param string $fingerprint The attached fingerprint. * @param string $payment_method_stripe_id The Stripe ID of the payment method used for this payment. * @param string $customer_id The WCPay Customer ID that owns the payment token. * * @throws Invalid_Payment_Method_Exception When no payment method is found in the provided request. */ public function __construct( string $payment_method, ?\WC_Order $order = null, ?Payment_Type $payment_type = null, ?\WC_Payment_Token $token = null, ?Payment_Initiated_By $payment_initiated_by = null, ?Payment_Capture_Type $manual_capture = null, ?string $cvc_confirmation = null, string $fingerprint = '', ?string $payment_method_stripe_id = null, ?string $customer_id = null ) { if ( empty( $payment_method ) && empty( $token ) && ! \WC_Payments::is_network_saved_cards_enabled() ) { // If network-wide cards are enabled, a payment method or token may not be specified and the platform default one will be used. throw new Invalid_Payment_Method_Exception( esc_html__( 'Invalid or missing payment details. Please ensure the provided payment method is correctly entered.', 'woocommerce-payments' ), 'payment_method_not_provided' ); } $this->payment_method = $payment_method; $this->order = $order; $this->token = $token; $this->payment_initiated_by = $payment_initiated_by ?? Payment_Initiated_By::CUSTOMER(); $this->manual_capture = $manual_capture ?? Payment_Capture_Type::AUTOMATIC(); $this->payment_type = $payment_type ?? Payment_Type::SINGLE(); $this->cvc_confirmation = $cvc_confirmation; $this->fingerprint = $fingerprint; $this->payment_method_stripe_id = $payment_method_stripe_id; $this->customer_id = $customer_id; } /** * Returns true if payment was initiated by the merchant, false otherwise. * * @return bool True if payment was initiated by the merchant, false otherwise. */ public function is_merchant_initiated(): bool { return $this->payment_initiated_by->equals( Payment_Initiated_By::MERCHANT() ); } /** * Returns the payment method ID or confirmation token. * * @return string The payment method ID or confirmation token. */ public function get_payment_method(): string { // Use the token if we have it. if ( $this->is_using_saved_payment_method() ) { return $this->token->get_token(); } return $this->payment_method; } /** * Returns whether the payment is using a confirmation token or a payment method. * * @see https://docs.stripe.com/payments/mobile/migration-confirmation-tokens * * @return bool True if using a confirmation token, false otherwise. */ public function is_using_confirmation_token(): bool { return 0 === strpos( $this->get_payment_method(), 'ctoken_' ); } /** * Returns the order object. * * @return ?\WC_Order The order object. */ public function get_order(): ?\WC_Order { return $this->order; } /** * Returns the payment token. * * TODO: Once php requirement is bumped to >= 7.1.0 change return type to ?\WC_Payment_Token * since the return type is nullable, as per * https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @return ?\WC_Payment_Token The payment token. */ public function get_payment_token(): ?\WC_Payment_Token { return $this->token; } /** * Update the payment token associated with this payment. * * @param \WC_Payment_Token $token The new payment token. */ public function set_token( \WC_Payment_Token $token ) { $this->token = $token; } /** * Returns true if the payment token is not empty, false otherwise. * * @return bool True if payment token is not empty, false otherwise. */ public function is_using_saved_payment_method(): bool { return ! empty( $this->token ); } /** * Returns true if the payment should be only authorized, false if it should be captured immediately. * * @return bool True if the payment should be only authorized, false if it should be captured immediately. */ public function is_using_manual_capture(): bool { return $this->manual_capture->equals( Payment_Capture_Type::MANUAL() ); } /** * Payment information constructor. * * @param array $request Associative array containing payment request information. * @param \WC_Order $order The order object. * @param Payment_Type $payment_type The type of the payment. * @param Payment_Initiated_By $payment_initiated_by Indicates whether the payment is merchant-initiated or customer-initiated. * @param Payment_Capture_Type $manual_capture Indicates whether the payment will be only authorized or captured immediately. * @param string $payment_method_stripe_id The Stripe ID of the payment method used for this payment. * * @throws \Exception - If no payment method is found in the provided request. */ public static function from_payment_request( array $request, ?\WC_Order $order = null, ?Payment_Type $payment_type = null, ?Payment_Initiated_By $payment_initiated_by = null, ?Payment_Capture_Type $manual_capture = null, ?string $payment_method_stripe_id = null ): Payment_Information { $payment_method = self::get_payment_method_from_request( $request ); $token = self::get_token_from_request( $request ); $cvc_confirmation = self::get_cvc_confirmation_from_request( $request ); $fingerprint = self::get_fingerprint_from_request( $request ); if ( isset( $request['is_woopay'] ) && $request['is_woopay'] ) { $order->add_meta_data( 'is_woopay', true, true ); $order->save_meta_data(); } $payment_information = new Payment_Information( $payment_method, $order, $payment_type, $token, $payment_initiated_by, $manual_capture, $cvc_confirmation, $fingerprint, $payment_method_stripe_id ); if ( self::PAYMENT_METHOD_ERROR === $payment_method ) { $error_message = $request['wcpay-payment-method-error-message'] ?? __( "We're not able to process this payment. Please try again later.", 'woocommerce-payments' ); $error_code = $request['wcpay-payment-method-error-code'] ?? 'unknown-error'; $error = new \WP_Error( $error_code, $error_message ); $payment_information->set_error( $error ); } return $payment_information; } /** * Extracts the payment method or confirmation token from the provided request. * * @param array $request Associative array containing payment request information. * * @return string */ public static function get_payment_method_from_request( array $request ): string { // Check for confirmation token first (new ECE flow), then fall back to payment method (legacy flow). foreach ( [ 'wcpay-confirmation-token', 'wcpay-payment-method', 'wcpay-payment-method-sepa' ] as $key ) { if ( ! empty( $request[ $key ] ) ) { $normalized = wc_clean( $request[ $key ] ); return is_string( $normalized ) ? $normalized : ''; } } return ''; } /** * Extract the payment token from the provided request. * * TODO: Once php requirement is bumped to >= 7.1.0 set return type to ?\WC_Payment_Token * since the return type is nullable, as per * https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @param array $request Associative array containing payment request information. * * @return \WC_Payment_Token|NULL */ public static function get_token_from_request( array $request ) { $payment_method = $request['payment_method'] ?? null; $token_request_key = 'wc-' . $payment_method . '-payment-token'; if ( ! isset( $request[ $token_request_key ] ) || 'new' === $request[ $token_request_key ] ) { return null; } //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $token = \WC_Payment_Tokens::get( wc_clean( $request[ $token_request_key ] ) ); // If the token doesn't belong to this gateway or the current user it's invalid. if ( ! $token || $payment_method !== $token->get_gateway_id() || $token->get_user_id() !== get_current_user_id() ) { return null; } return $token; } /** * Extract the payment CVC confirmation from the provided request. * * @param array $request Associative array containing payment request information. * * @return string|NULL */ public static function get_cvc_confirmation_from_request( array $request ) { $payment_method = $request['payment_method'] ?? null; if ( null === $payment_method ) { return null; } $cvc_request_key = 'wc-' . $payment_method . '-payment-cvc-confirmation'; if ( ! isset( $request[ $cvc_request_key ] ) || 'new' === $request[ $cvc_request_key ] ) { return null; } return $request[ $cvc_request_key ]; } /** * Extracts the fingerprint data from the provided request. * * @param array $request Associative array containing payment request information. * * @return string */ public static function get_fingerprint_from_request( array $request ) { if ( ! empty( $request['wcpay-fingerprint'] ) ) { $normalized = wc_clean( $request['wcpay-fingerprint'] ); return is_string( $normalized ) ? $normalized : ''; } return ''; } /** * Changes the type of the payment. * * @param Payment_Type $type The new type. */ public function set_payment_type( $type ) { $this->payment_type = $type; } /** * Retrieves the type of the payment. * * @return Payment_Type The payment type. */ public function get_payment_type() { return $this->payment_type; } /** * Forces the payment method to be saved to merchant store when the payment gets processed. */ public function must_save_payment_method_to_store() { $this->save_payment_method_to_store = true; } /** * Forces the payment method to be saved to platform when the payment gets processed. */ public function must_save_payment_method_to_platform() { $this->save_payment_method_to_platform = true; } /** * Indicates whether the payment method needs to be saved to the store for later usage. * * @return bool The flag. */ public function should_save_payment_method_to_store() { return ! $this->is_using_saved_payment_method() && $this->save_payment_method_to_store; } /** * Indicates whether the payment method needs to be saved to the platform for later usage. * * @return bool The flag. */ public function should_save_payment_method_to_platform() { return ! $this->is_using_saved_payment_method() && $this->save_payment_method_to_platform; } /** * Mark that we are changing payment for subscriptions order or not. * * @param bool $is_changing_payment_method_for_subscription Whether or not we are changing payment for subscriptions order. */ public function set_is_changing_payment_method_for_subscription( bool $is_changing_payment_method_for_subscription ) { $this->is_changing_payment_method_for_subscription = $is_changing_payment_method_for_subscription; } /** * Returns the flag of whether or not we are changing payment for subscriptions order. * * @return bool Whether or not we are changing payment for subscriptions order. */ public function is_changing_payment_method_for_subscription(): bool { return $this->is_changing_payment_method_for_subscription; } /** * Returns the payment method CVC confirmation. * * @return string|NULL The payment method CVC confirmation. */ public function get_cvc_confirmation() { return $this->cvc_confirmation; } /** * Returns the attached fingerprint. * * @return string The attached fingerprint. */ public function get_fingerprint() { return $this->fingerprint; } /** * Returns the Stripe ID of payment method. * * @return string The Stripe ID of payment method. */ public function get_payment_method_stripe_id() { return $this->payment_method_stripe_id; } /** * Returns the WCPay Customer ID that owns the payment token. * * @return string The WCPay Customer ID. */ public function get_customer_id() { return $this->customer_id; } /** * Sets the error data. * * @param \WP_Error $error The error to be set. * @return void */ public function set_error( \WP_Error $error ) { $this->error = $error; } /** * Returns the error data. * * @return ?\WP_Error */ public function get_error() { return $this->error; } /** * Returns true if the payment method is an offline payment method, false otherwise. * * @return bool True if the payment method is an offline payment method, false otherwise. */ public function is_offline_payment_method(): bool { return in_array( $this->payment_method_stripe_id, Payment_Method::OFFLINE_PAYMENT_METHODS, true ); } } includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php 0000777 00000021204 15251156605 0025626 0 ustar 00 <?php /** * Class WC_Payments_Express_Checkout_Button_Display_Handler * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Class WC_Payments_Express_Checkout_Button_Display_Handler */ class WC_Payments_Express_Checkout_Button_Display_Handler { /** * WC_Payment_Gateway_WCPay instance. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * Instance of WC_Payments_WooPay_Button_Handler, created in init function * * @var WC_Payments_WooPay_Button_Handler */ private $platform_checkout_button_handler; /** * Instance of WC_Payments_Express_Checkout_Button_Handler, created in init function * * @var WC_Payments_Express_Checkout_Button_Handler */ private $express_checkout_button_handler; /** * Express Checkout Helper instance. * * @var WC_Payments_Express_Checkout_Ajax_Handler */ private $express_checkout_ajax_handler; /** * Express Checkout Helper instance. * * @var WC_Payments_Express_Checkout_Button_Helper */ private $express_checkout_helper; /** * Initialize class actions. * * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. * @param WC_Payments_WooPay_Button_Handler $platform_checkout_button_handler Platform checkout button handler. * @param WC_Payments_Express_Checkout_Button_Handler $express_checkout_button_handler Express Checkout Element button handler. * @param WC_Payments_Express_Checkout_Ajax_Handler $express_checkout_ajax_handler Express checkout ajax handlers. * @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper Express checkout button helper. */ public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_WooPay_Button_Handler $platform_checkout_button_handler, WC_Payments_Express_Checkout_Button_Handler $express_checkout_button_handler, WC_Payments_Express_Checkout_Ajax_Handler $express_checkout_ajax_handler, WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper ) { $this->gateway = $gateway; $this->platform_checkout_button_handler = $platform_checkout_button_handler; $this->express_checkout_button_handler = $express_checkout_button_handler; $this->express_checkout_ajax_handler = $express_checkout_ajax_handler; $this->express_checkout_helper = $express_checkout_helper; } /** * Initializes this class, its dependencies, and its hooks. * * @return void */ public function init() { $this->platform_checkout_button_handler->init(); $this->express_checkout_button_handler->init(); $is_woopay_enabled = WC_Payments_Features::is_woopay_enabled(); $is_payment_request_enabled = $this->gateway->is_payment_request_enabled(); $is_amazon_pay_enabled = $this->express_checkout_helper->can_use_amazon_pay(); if ( $is_woopay_enabled ) { add_action( 'wc_ajax_wcpay_add_to_cart', [ $this->express_checkout_ajax_handler, 'ajax_add_to_cart' ] ); } if ( $is_woopay_enabled || $is_payment_request_enabled || $is_amazon_pay_enabled ) { add_action( 'woocommerce_after_add_to_cart_form', [ $this, 'display_express_checkout_buttons' ], 1 ); add_action( 'woocommerce_proceed_to_checkout', [ $this, 'display_express_checkout_buttons' ], 21 ); add_action( 'woocommerce_checkout_before_customer_details', [ $this, 'display_express_checkout_buttons' ], 1 ); add_action( 'woocommerce_pay_order_before_payment', [ $this, 'display_express_checkout_buttons' ], 1 ); } add_filter( 'wcpay_tracks_event_properties', [ $this, 'record_all_ece_tracks_events' ], 10, 2 ); if ( $this->is_pay_for_order_flow_supported() ) { add_action( 'wp_enqueue_scripts', [ $this, 'add_pay_for_order_params_to_js_config' ], 5 ); } } /** * Display express checkout separator only when express buttons are displayed. * * @param bool $separator_starts_hidden Whether the separator should start hidden. * @return void */ public function display_express_checkout_separator_if_necessary( $separator_starts_hidden = false ) { if ( $this->express_checkout_helper->is_checkout() ) { ?> <p id="wcpay-express-checkout-button-separator" style="margin-top:1.5em;text-align:center;<?php echo $separator_starts_hidden ? 'display:none;' : ''; ?>">— <?php esc_html_e( 'OR', 'woocommerce-payments' ); ?> —</p> <?php } } /** * Display express checkout separator only when express buttons are displayed. * * @return void */ public function display_express_checkout_buttons() { $should_show_woopay = $this->platform_checkout_button_handler->should_show_woopay_button(); $should_show_express_checkout_button = $this->express_checkout_helper->should_show_express_checkout_button(); // When Express Checkout button is enabled, we need the separator markup on the page, but hidden in case the browser doesn't have any express payment methods to display. // More details: https://github.com/Automattic/woocommerce-payments/pull/5399#discussion_r1073633776. $separator_starts_hidden = ! $should_show_woopay; if ( $should_show_woopay || $should_show_express_checkout_button ) { ?> <div class='wcpay-express-checkout-wrapper' > <?php if ( ! $this->express_checkout_helper->is_pay_for_order_page() || $this->is_pay_for_order_flow_supported() ) { $this->platform_checkout_button_handler->display_woopay_button_html(); } $this->express_checkout_button_handler->display_express_checkout_button_html(); if ( is_cart() ) { add_action( 'woocommerce_after_cart', [ $this, 'add_order_attribution_inputs' ], 1 ); } else { $this->add_order_attribution_inputs(); } $this->display_express_checkout_separator_if_necessary( $separator_starts_hidden ); ?> </div > <?php } } /** * Add order attribution inputs to the page. * * @return void */ public function add_order_attribution_inputs() { echo '<wc-order-attribution-inputs id="wcpay-express-checkout__order-attribution-inputs"></wc-order-attribution-inputs>'; } /** * Check if the pay-for-order flow is supported. * * @return bool */ private function is_pay_for_order_flow_supported() { return ( class_exists( '\Automattic\WooCommerce\Blocks\Package' ) && version_compare( \Automattic\WooCommerce\Blocks\Package::get_version(), '11.1.0', '>=' ) ); } /** * Check if WooPay is enabled. * * @return bool */ public function is_woopay_enabled() { return $this->platform_checkout_button_handler->is_woopay_enabled(); } /** * Add the Pay for order params to the JS config. */ public function add_pay_for_order_params_to_js_config() { global $wp; $order_id = $wp->query_vars['order-pay'] ?? null; if ( ! $order_id ) { return; } $order = wc_get_order( $order_id ); // phpcs:disable WordPress.Security.NonceVerification if ( isset( $_GET['pay_for_order'] ) && isset( $_GET['key'] ) && current_user_can( 'pay_for_order', $order_id ) ) { add_filter( 'wcpay_payment_fields_js_config', function ( $js_config ) use ( $order ) { $session = wc()->session; $session_email = ''; if ( is_a( $session, WC_Session::class ) ) { $customer = $session->get( 'customer' ); $session_email = is_array( $customer ) && isset( $customer['email'] ) ? $customer['email'] : ''; } $user_email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL ) ) ) : $session_email; $js_config['order_id'] = $order->get_id(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated $js_config['pay_for_order'] = sanitize_text_field( wp_unslash( $_GET['pay_for_order'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated $js_config['key'] = sanitize_text_field( wp_unslash( $_GET['key'] ) ); $js_config['billing_email'] = current_user_can( 'read_private_shop_orders' ) || ( get_current_user_id() !== 0 && $order->get_customer_id() === get_current_user_id() ) ? $order->get_billing_email() : $user_email; return $js_config; } ); } // phpcs:enable WordPress.Security.NonceVerification } /** * Record all ECE tracks events by adding the track_on_all_stores flag to the event. * * @param array $properties Event properties. * @param string $event_name Event name. * @return array */ public function record_all_ece_tracks_events( $properties, $event_name ) { $tracked_events_prefixes = [ 'wcpay_applepay', 'wcpay_gpay', ]; foreach ( $tracked_events_prefixes as $prefix ) { if ( strpos( $event_name, $prefix ) === 0 ) { $properties['record_event_data']['track_on_all_stores'] = true; break; } } return $properties; } } includes/express-checkout/class-wc-payments-express-checkout-ajax-handler.php 0000777 00000046757 15251156605 0023617 0 ustar 00 <?php /** * Class WC_Payments_Express_Checkout_Ajax_Handler * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; } use WCPay\Constants\Country_Code; use WCPay\Exceptions\Invalid_Price_Exception; use WCPay\Logger; /** * WC_Payments_Express_Checkout_Ajax_Handler class. */ class WC_Payments_Express_Checkout_Ajax_Handler { /** * WC_Payments_Express_Checkout_Button_Helper instance. * * @var WC_Payments_Express_Checkout_Button_Helper */ private $express_checkout_button_helper; /** * Constructor. * * @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_button_helper Express checkout button helper. */ public function __construct( WC_Payments_Express_Checkout_Button_Helper $express_checkout_button_helper ) { $this->express_checkout_button_helper = $express_checkout_button_helper; } /** * Initialize hooks. * * @return void */ public function init() { if ( function_exists( 'woocommerce_store_api_register_update_callback' ) ) { woocommerce_store_api_register_update_callback( [ 'namespace' => 'woopayments/express-checkout/refresh-ui', // do nothing, this callback is needed just to refresh the UI. 'callback' => '__return_null', ] ); } add_action( 'woocommerce_store_api_checkout_update_order_from_request', [ $this, 'tokenized_cart_set_payment_method_type', ], 10, 2 ); add_filter( 'rest_pre_dispatch', [ $this, 'tokenized_cart_store_api_address_normalization' ], 10, 3 ); add_filter( 'woocommerce_get_country_locale', [ $this, 'modify_country_locale_for_express_checkout' ], 20 ); } /** * Adds the current product to the cart. Used on product detail page. */ public function ajax_add_to_cart() { check_ajax_referer( 'wcpay-add-to-cart', 'security' ); if ( ! defined( 'WOOCOMMERCE_CART' ) ) { define( 'WOOCOMMERCE_CART', true ); } WC()->shipping->reset_shipping(); $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; $product = wc_get_product( $product_id ); if ( ! $product ) { wp_send_json( [ 'error' => [ 'code' => 'invalid_product_id', 'message' => __( 'Invalid product id', 'woocommerce-payments' ), ], ], 404 ); return; } $quantity = $this->express_checkout_button_helper->get_quantity(); $product_type = $product->get_type(); $is_add_to_cart_valid = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); if ( ! $is_add_to_cart_valid ) { // Some extensions error messages needs to be // submitted to show error messages. wp_send_json( [ 'error' => true, 'submit' => true, ], 400 ); return; } // First empty the cart to prevent wrong calculation. WC()->cart->empty_cart(); if ( ( 'variable' === $product_type || 'variable-subscription' === $product_type ) && isset( $_POST['attributes'] ) ) { $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) ); $data_store = WC_Data_Store::load( 'product' ); $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); WC()->cart->add_to_cart( $product->get_id(), $quantity, $variation_id, $attributes ); } if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation', 'booking', 'bundle', 'mix-and-match' ], true ) ) { $allowed_item_data = [ // Teams for WooCommerce Memberships fields. 'team_name', 'team_owner_takes_seat', ]; $item_data = []; foreach ( $allowed_item_data as $item ) { if ( isset( $_POST[ $item ] ) ) { $item_data[ $item ] = wc_clean( wp_unslash( $_POST[ $item ] ) ); } } WC()->cart->add_to_cart( $product->get_id(), $quantity, 0, [], $item_data ); } WC()->cart->calculate_totals(); if ( 'booking' === $product_type ) { $booking_id = $this->express_checkout_button_helper->get_booking_id_from_cart(); } $data = []; $data += $this->express_checkout_button_helper->build_display_items(); $data['result'] = 'success'; if ( ! empty( $booking_id ) ) { $data['bookingId'] = $booking_id; } wp_send_json( $data ); } /** * Updates the checkout order based on the request, to set the Apple Pay/Google Pay payment method title. * * @param \WC_Order $order The order to be updated. * @param \WP_REST_Request $request Store API request to update the order. */ public function tokenized_cart_set_payment_method_type( \WC_Order $order, \WP_REST_Request $request ) { if ( ! isset( $request['payment_method'] ) || 'woocommerce_payments' !== $request['payment_method'] ) { return; } if ( empty( $request['payment_data'] ) ) { return; } $payment_data = []; foreach ( $request['payment_data'] as $data ) { $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] ); } if ( empty( $payment_data['express_payment_type'] ) ) { return; } $express_payment_type = wc_clean( wp_unslash( $payment_data['express_payment_type'] ) ); $payment_method_title = $this->get_payment_method_title_from_definition( $express_payment_type ); // fallback, just in case. if ( ! $payment_method_title ) { $payment_method_title = 'Payment Request'; } $suffix = apply_filters( 'wcpay_payment_request_payment_method_title_suffix', 'WooPayments' ); if ( ! empty( $suffix ) ) { $suffix = " ($suffix)"; } $order->set_payment_method_title( $payment_method_title . $suffix ); $order->update_meta_data( '_wcpay_express_checkout_payment_method', $express_payment_type ); } /** * Get the payment method title from the definition. * * @param string $payment_method_id The payment method ID (e.g., 'apple_pay', 'google_pay'). * @return string|null The payment method title or null if not found. */ private function get_payment_method_title_from_definition( $payment_method_id ) { $payment_method = WC_Payments::get_payment_method_by_id( $payment_method_id ); if ( $payment_method && method_exists( $payment_method, 'get_title' ) ) { return $payment_method->get_title(); } return null; } /** * Google Pay/Apple Pay parameters for address data might need some massaging for some of the countries. * Ensuring that the Store API doesn't throw a `rest_invalid_param` error message for some of those scenarios. * * @param mixed $response Response to replace the requested version with. * @param \WP_REST_Server $server Server instance. * @param \WP_REST_Request $request Request used to generate the response. * * @return mixed */ public function tokenized_cart_store_api_address_normalization( $response, $server, $request ) { if ( 'true' !== $request->get_header( 'X-WooPayments-Tokenized-Cart' ) ) { return $response; } // header added as additional layer of security. $nonce = $request->get_header( 'X-WooPayments-Tokenized-Cart-Nonce' ); if ( ! wp_verify_nonce( $nonce, 'woopayments_tokenized_cart_nonce' ) ) { return $response; } // This route is used to get shipping rates. // Google Pay/Apple Pay might provide us with "trimmed" zip codes. // If that's the case, let's temporarily allow to skip the zip code validation, in order to get some shipping rates. $is_update_customer_route = $request->get_route() === '/wc/store/v1/cart/update-customer'; if ( $is_update_customer_route ) { add_filter( 'woocommerce_validate_postcode', [ $this, 'maybe_skip_postcode_validation' ], 10, 3 ); } if ( isset( $request['shipping_address'] ) && is_array( $request['shipping_address'] ) ) { $shipping_address = $request['shipping_address']; $shipping_address = $this->transform_ece_address_state_data( $shipping_address ); // on the "update customer" route, Google Pay/Apple Pay might provide redacted postcode data. // we need to modify the zip code to ensure that shipping zone identification still works. if ( $is_update_customer_route ) { $shipping_address = $this->transform_ece_address_postcode_data( $shipping_address ); } $request->set_param( 'shipping_address', $shipping_address ); } if ( isset( $request['billing_address'] ) && is_array( $request['billing_address'] ) ) { $billing_address = $request['billing_address']; $billing_address = $this->transform_ece_address_state_data( $billing_address ); // on the "update customer" route, Google Pay/Apple Pay might provide redacted postcode data. // we need to modify the zip code to ensure that shipping zone identification still works. if ( $is_update_customer_route ) { $billing_address = $this->transform_ece_address_postcode_data( $billing_address ); } $request->set_param( 'billing_address', $billing_address ); } return $response; } /** * Allows certain "redacted" postcodes for some countries to bypass WC core validation. * * @param bool $valid Whether the postcode is valid. * @param string $postcode The postcode in question. * @param string $country The country for the postcode. * * @return bool */ public function maybe_skip_postcode_validation( $valid, $postcode, $country ) { if ( ! in_array( $country, [ Country_Code::UNITED_KINGDOM, Country_Code::CANADA ], true ) ) { return $valid; } // We padded the string with `0` in the `get_normalized_postal_code` method. // It's a flimsy check, but better than nothing. // Plus, this check is only made for the scenarios outlined in the `tokenized_cart_store_api_address_normalization` method. if ( substr( $postcode, - 1 ) === '0' ) { return true; } return $valid; } /** * Transform a Google Pay/Apple Pay state address data fields into values that are valid for WooCommerce. * * @param array $address The address to normalize from the Google Pay/Apple Pay request. * * @return array */ private function transform_ece_address_state_data( $address ) { $country = $address['country'] ?? ''; if ( empty( $country ) ) { return $address; } // Due to a bug in Apple Pay, the "Region" part of a Hong Kong address is delivered in // `shipping_postcode`, so we need some special case handling for that. According to // our sources at Apple Pay people will sometimes use the district or even sub-district // for this value. As such we check against all regions, districts, and sub-districts // with both English and Mandarin spelling. // // @reykjalin: The check here is quite elaborate in an attempt to make sure this doesn't break once // Apple Pay fixes the bug that causes address values to be in the wrong place. Because of that the // algorithm becomes: // 1. Use the supplied state if it's valid (in case Apple Pay bug is fixed) // 2. Use the value supplied in the postcode if it's a valid HK region (equivalent to a WC state). // 3. Fall back to the value supplied in the state. This will likely cause a validation error, in // which case a merchant can reach out to us so we can either: 1) add whatever the customer used // as a state to our list of valid states; or 2) let them know the customer must spell the state // in some way that matches our list of valid states. // // @reykjalin: This HK specific sanitazation *should be removed* once Apple Pay fix // the address bug. More info on that in pc4etw-bY-p2. if ( Country_Code::HONG_KONG === $country ) { include_once WCPAY_ABSPATH . 'includes/constants/class-express-checkout-hong-kong-states.php'; $state = $address['state'] ?? ''; if ( ! \WCPay\Constants\Express_Checkout_Hong_Kong_States::is_valid_state( strtolower( $state ) ) ) { $postcode = $address['postcode'] ?? ''; if ( strtolower( $postcode ) === 'hongkong' ) { $postcode = 'hong kong'; } if ( \WCPay\Constants\Express_Checkout_Hong_Kong_States::is_valid_state( strtolower( $postcode ) ) ) { $address['state'] = $postcode; } } } // States from Apple Pay or Google Pay are in long format, we need their short format. $state = $address['state'] ?? ''; if ( ! empty( $state ) ) { $address['state'] = $this->get_normalized_state( $state, $country ); } return $address; } /** * Gets the normalized state/county field because in some * cases, the state/county field is formatted differently from * what WC is expecting and throws an error. An example * for Ireland, the county dropdown in Chrome shows "Co. Clare" format. * * @param string $state Full state name or an already normalized abbreviation. * @param string $country Two-letter country code. * * @return string Normalized state abbreviation. */ private function get_normalized_state( $state, $country ) { // If it's empty or already normalized, skip. if ( ! $state || $this->is_normalized_state( $state, $country ) ) { return $state; } // Try to match state from the Express Checkout API list of states. $state = $this->get_normalized_state_from_ece_states( $state, $country ); // If it's normalized, return. if ( $this->is_normalized_state( $state, $country ) ) { return $state; } // If the above doesn't work, fallback to matching against the list of translated // states from WooCommerce. return $this->get_normalized_state_from_wc_states( $state, $country ); } /** * Checks if given state is normalized. * * @param string $state State. * @param string $country Two-letter country code. * * @return bool Whether state is normalized or not. */ private function is_normalized_state( $state, $country ) { $wc_states = WC()->countries->get_states( $country ); return is_array( $wc_states ) && array_key_exists( $state, $wc_states ); } /** * Get normalized state from Express Checkout API dropdown list of states. * * @param string $state Full state name or state code. * @param string $country Two-letter country code. * * @return string Normalized state or original state input value. */ private function get_normalized_state_from_ece_states( $state, $country ) { // Include Express Checkout Element API State list for compatibility with WC countries/states. include_once WCPAY_ABSPATH . 'includes/constants/class-express-checkout-element-states.php'; $pr_states = \WCPay\Constants\Express_Checkout_Element_States::STATES; if ( ! isset( $pr_states[ $country ] ) ) { return $state; } foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) { $sanitized_state_string = $this->express_checkout_button_helper->sanitize_string( $state ); // Checks if input state matches with Express Checkout state code (0), name (1) or localName (2). if ( ( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->express_checkout_button_helper->sanitize_string( $pr_state[0] ) ) || ( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->express_checkout_button_helper->sanitize_string( $pr_state[1] ) ) || ( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->express_checkout_button_helper->sanitize_string( $pr_state[2] ) ) ) { return $wc_state_abbr; } } return $state; } /** * Get normalized state from WooCommerce list of translated states. * * @param string $state Full state name or state code. * @param string $country Two-letter country code. * * @return string Normalized state or original state input value. */ private function get_normalized_state_from_wc_states( $state, $country ) { $wc_states = WC()->countries->get_states( $country ); if ( is_array( $wc_states ) ) { foreach ( $wc_states as $wc_state_abbr => $wc_state_value ) { if ( preg_match( '/' . preg_quote( $wc_state_value, '/' ) . '/i', $state ) ) { return $wc_state_abbr; } } } return $state; } /** * Transform a Google Pay/Apple Pay postcode address data fields into values that are valid for WooCommerce. * * @param array $address The address to normalize from the Google Pay/Apple Pay request. * * @return array */ private function transform_ece_address_postcode_data( $address ) { $country = $address['country'] ?? ''; if ( empty( $country ) ) { return $address; } // Normalizes postal code in case of redacted data from Apple Pay or Google Pay. $postcode = $address['postcode'] ?? ''; if ( ! empty( $postcode ) ) { $address['postcode'] = $this->get_normalized_postal_code( $postcode, $country ); } return $address; } /** * Normalizes postal code in case of redacted data from Apple Pay. * * @param string $postcode Postal code. * @param string $country Country. */ private function get_normalized_postal_code( $postcode, $country ) { /** * Currently, Apple Pay truncates the UK and Canadian postal codes to the first few characters respectively * when passing it back from the shippingcontactselected object. This causes WC to invalidate * the postal code and not calculate shipping zones correctly. */ if ( Country_Code::UNITED_KINGDOM === $country ) { $cleaned_postcode = substr( preg_replace( '/[^A-Za-z0-9]/', '', $postcode ), 0, 7 ); // the minimum length for a GB postcode is 5 (2 characters for the outward code, 3 for the inward code) // if the postcode is not redacted, avoid padding it. if ( strlen( $cleaned_postcode ) >= 5 ) { return $cleaned_postcode; } // now, the juicy part: GB postcode units have a variable length, 5 to 7 characters (excluding the space). // they consist of two main parts: the "outward code" and the "inward code". // the "outward code" has a variable length, between two and four characters. // the "inward code" always has 3 characters. // Google Pay/Apple Pay might redact GB postcode units to just the "outward code". // but WC Core expects a full postcode unit to return shipping rates. // since we can't interfere with the rate calculation, // we are padding the (redacted) outward code with `0`s to have a full length postcode unit, // to be used for shipping rates calculations. // Replaces a redacted `N1C` string with something like `N1C000`. return $cleaned_postcode . '000'; } if ( Country_Code::CANADA === $country ) { // Replaces a redacted string with something like H3B000. return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '0' ); } return $postcode; } /** * Modify country locale settings to handle express checkout address requirements. * * @param array $locales Array of country locale settings. * @return array Modified locales array. */ public function modify_country_locale_for_express_checkout( $locales ) { // Only modify locale settings if this is an express checkout AJAX request. if ( ! $this->is_express_checkout_context() ) { return $locales; } include_once WCPAY_ABSPATH . 'includes/constants/class-express-checkout-element-states.php'; // For countries that don't have state fields, make the state field optional. foreach ( \WCPay\Constants\Express_Checkout_Element_States::COUNTRIES_WITHOUT_STATES as $country_code ) { $locales[ $country_code ]['state']['required'] = false; } return $locales; } /** * Check if we're in an express checkout context. * * @return bool True if we're in an express checkout context, false otherwise. */ private function is_express_checkout_context() { // Only proceed if this is a Store API request. if ( ! WC_Payments_Utils::is_store_api_request() ) { return false; } // Check for the 'X-WooPayments-Tokenized-Cart' header using superglobals. if ( 'true' !== sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART'] ?? '' ) ) ) { return false; } // Verify the nonce from the 'X-WooPayments-Tokenized-Cart-Nonce' header using superglobals. $nonce = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WOOPAYMENTS_TOKENIZED_CART_NONCE'] ?? '' ) ); if ( ! wp_verify_nonce( $nonce, 'woopayments_tokenized_cart_nonce' ) ) { return false; } return true; } } includes/express-checkout/class-wc-payments-express-checkout-button-helper.php 0000777 00000070067 15251156605 0024040 0 ustar 00 <?php /** * Class WC_Payments_Express_Checkout_Button_Helper * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; use WCPay\Exceptions\Invalid_Price_Exception; use WCPay\Logger; use WCPay\PaymentMethods\Configs\Definitions\AmazonPayDefinition; /** * Express Checkout Button Helper class. */ class WC_Payments_Express_Checkout_Button_Helper { /** * WC_Payment_Gateway_WCPay instance. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * WC_Payments_Account instance to get information about the account * * @var WC_Payments_Account */ private $account; /** * Initialize class actions. * * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. * @param WC_Payments_Account $account Account information. */ public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Account $account ) { $this->gateway = $gateway; $this->account = $account; } /** * Gets the booking id from the cart. * It's expected that the cart only contains one item which was added via ajax_add_to_cart. * Used to remove the booking from WC Bookings in-cart status. * * @return int|false */ public function get_booking_id_from_cart() { $cart = WC()->cart->get_cart(); $cart_item = reset( $cart ); if ( $cart_item && isset( $cart_item['booking']['_booking_id'] ) ) { return $cart_item['booking']['_booking_id']; } return false; } /** * Builds the line items to pass to Express Checkout * * @param boolean $itemized_display_items Indicates whether to show subtotals or itemized views. */ public function build_display_items( $itemized_display_items = false ) { if ( ! defined( 'WOOCOMMERCE_CART' ) ) { define( 'WOOCOMMERCE_CART', true ); } $items = []; $discounts = 0; $currency = get_woocommerce_currency(); // Default show only subtotal instead of itemization. if ( ! apply_filters( 'wcpay_payment_request_hide_itemization', ! $itemized_display_items ) ) { foreach ( WC()->cart->get_cart() as $cart_item ) { $amount = $cart_item['line_subtotal']; $quantity_label = 1 < $cart_item['quantity'] ? ' (x' . $cart_item['quantity'] . ')' : ''; $product_name = $cart_item['data']->get_name(); $item_tax = $this->cart_prices_include_tax() ? ( $cart_item['line_subtotal_tax'] ?? 0 ) : 0; $item = [ 'label' => $product_name . $quantity_label, 'amount' => WC_Payments_Utils::prepare_amount( $amount + $item_tax, $currency ), ]; $items[] = $item; } } if ( version_compare( WC_VERSION, '3.2', '<' ) ) { $discounts = wc_format_decimal( WC()->cart->get_cart_discount_total(), WC()->cart->dp ); } else { $applied_coupons = array_values( WC()->cart->get_coupon_discount_totals() ); foreach ( $applied_coupons as $amount ) { $discounts += (float) $amount; } } $discounts = wc_format_decimal( $discounts, WC()->cart->dp ); $tax = wc_format_decimal( WC()->cart->tax_total + WC()->cart->shipping_tax_total, WC()->cart->dp ); $shipping = wc_format_decimal( WC()->cart->shipping_total, WC()->cart->dp ); $items_total = wc_format_decimal( WC()->cart->cart_contents_total, WC()->cart->dp ) + $discounts; $order_total = version_compare( WC_VERSION, '3.2', '<' ) ? wc_format_decimal( $items_total + $tax + $shipping - $discounts, WC()->cart->dp ) : WC()->cart->get_total( '' ); if ( ! $this->cart_prices_include_tax() ) { $items[] = [ 'label' => esc_html( __( 'Tax', 'woocommerce-payments' ) ), 'amount' => WC_Payments_Utils::prepare_amount( $tax, $currency ), ]; } if ( WC()->cart->needs_shipping() ) { $shipping_tax = $this->cart_prices_include_tax() ? WC()->cart->shipping_tax_total : 0; $items[] = [ 'key' => 'total_shipping', 'label' => esc_html( __( 'Shipping', 'woocommerce-payments' ) ), 'amount' => WC_Payments_Utils::prepare_amount( $shipping + $shipping_tax, $currency ), ]; } if ( WC()->cart->has_discount() ) { $items[] = [ 'key' => 'total_discount', 'label' => esc_html( __( 'Discount', 'woocommerce-payments' ) ), 'amount' => WC_Payments_Utils::prepare_amount( $discounts, $currency ), ]; } if ( version_compare( WC_VERSION, '3.2', '<' ) ) { $cart_fees = WC()->cart->fees; } else { $cart_fees = WC()->cart->get_fees(); } // Include fees and taxes as display items. foreach ( $cart_fees as $fee ) { $items[] = [ 'label' => $fee->name, 'amount' => WC_Payments_Utils::prepare_amount( $fee->amount, $currency ), ]; } return [ 'displayItems' => $items, 'total' => [ 'label' => $this->get_total_label(), 'amount' => max( 0, apply_filters( 'wcpay_calculated_total', WC_Payments_Utils::prepare_amount( $order_total, $currency ), $order_total, WC()->cart ) ), 'pending' => false, ], ]; } /** * Whether tax should be displayed on separate line in cart. * returns true if tax is disabled or display of tax in checkout is set to inclusive. * * @return boolean */ public function cart_prices_include_tax() { return ! wc_tax_enabled() || 'incl' === get_option( 'woocommerce_tax_display_cart' ); } /** * Gets total label. * * @return string */ public function get_total_label() { // Get statement descriptor from API/cached account data. $statement_descriptor = $this->account->get_statement_descriptor(); return str_replace( "'", '', $statement_descriptor ) . apply_filters( 'wcpay_payment_request_total_label_suffix', ' (via WooCommerce)' ); } /** * Gets quantity from request. * * @return int */ public function get_quantity() { // Express Checkout Element sends the quantity as qty. WooPay sends it as quantity. if ( isset( $_POST['quantity'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return absint( $_POST['quantity'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } elseif ( isset( $_POST['qty'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return absint( $_POST['qty'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } else { return 1; } } /** * Checks if this is a product page or content contains a product_page shortcode. * * @return boolean */ public function is_product() { return is_product() || wc_post_content_has_shortcode( 'product_page' ); } /** * Checks if this is the Pay for Order page. * * @return boolean */ public function is_pay_for_order_page() { return is_checkout() && isset( $_GET['pay_for_order'] ); // phpcs:ignore WordPress.Security.NonceVerification } /** * Checks if this is the cart page or content contains a cart block. * * @return boolean */ public function is_cart() { return is_cart() || has_block( 'woocommerce/cart' ); } /** * Checks if this is the checkout page or content contains a cart block. * * @return boolean */ public function is_checkout() { return is_checkout() || has_block( 'woocommerce/checkout' ); } /** * Checks if a specific express checkout method is enabled at a given location. * * Uses the new location-centric settings (express_checkout_{location}_methods). * * @param string $location Location (product, cart, checkout). * @param string $method_id Method identifier (payment_request, woopay, amazon_pay, link). * @return boolean */ public function is_express_checkout_method_enabled_at( $location, $method_id ) { $enabled_methods = $this->gateway->get_option( "express_checkout_{$location}_methods" ); if ( $enabled_methods && is_array( $enabled_methods ) ) { return in_array( $method_id, $enabled_methods, true ); } return false; } /** * Checks if Amazon Pay can be used in Express Checkout. * * This validates: * - Feature flag is enabled * - Gateway exists and is enabled * - Account has Amazon Pay fees configured (indicates availability) * - Tax settings are compatible * - Currency is supported for the account country * * @return boolean */ public function can_use_amazon_pay() { if ( ! WC_Payments_Features::is_amazon_pay_enabled() ) { return false; } $amazon_pay_gateway = WC_Payments::get_payment_gateway_by_id( AmazonPayDefinition::get_id() ); if ( ! $amazon_pay_gateway ) { return false; } if ( ! $amazon_pay_gateway->is_enabled() ) { return false; } // Check if Amazon Pay has fees configured (indicates it's actually available for the account). $methods_with_fees = array_keys( $this->account->get_fees() ); if ( ! in_array( AmazonPayDefinition::get_id(), $methods_with_fees, true ) ) { return false; } // Amazon Pay doesn't support taxes based on billing address. if ( wc_tax_enabled() && 'billing' === get_option( 'woocommerce_tax_based_on' ) && ! $this->is_pay_for_order_page() ) { return false; } $currency = get_woocommerce_currency(); $account_country = $this->account->get_account_country(); return AmazonPayDefinition::is_available_for( $currency, $account_country ); } /** * Checks if any express checkout method (Google/Apple Pay or Amazon Pay) is enabled at a given location in settings. * * This only checks location settings (express_checkout_{location}_methods), not feature flags. * Feature flags are checked at initialization and in get_enabled_express_checkout_methods_for_context(). * * @param string $location Location (product, cart, checkout). * @return boolean */ public function is_any_express_checkout_method_enabled_at( $location ) { // Check Google Pay / Apple Pay (payment_request). if ( $this->is_express_checkout_method_enabled_at( $location, 'payment_request' ) ) { return true; } // Check Amazon Pay. if ( $this->is_express_checkout_method_enabled_at( $location, 'amazon_pay' ) ) { return true; } return false; } /** * Gets the list of enabled express checkout methods for the current page context. * * This method checks: * 1. The current page context (product, cart, checkout) * 2. The location settings (express_checkout_{location}_methods) * 3. The feature flags (is_payment_request_enabled, is_amazon_pay_enabled) * 4. Currency availability (e.g., Amazon Pay checks currency restrictions) * * @return array Array of enabled method IDs (e.g., ['payment_request', 'amazon_pay']). */ public function get_enabled_express_checkout_methods_for_context() { $enabled_methods = []; $context = $this->get_button_context(); // If no valid context, return an empty array. if ( empty( $context ) ) { return $enabled_methods; } // Check Google Pay / Apple Pay (payment_request). if ( $this->gateway->is_payment_request_enabled() && $this->is_express_checkout_method_enabled_at( $context, 'payment_request' ) ) { $enabled_methods[] = 'payment_request'; } // Check Amazon Pay. if ( $this->can_use_amazon_pay() && $this->is_express_checkout_method_enabled_at( $context, 'amazon_pay' ) ) { $enabled_methods[] = 'amazon_pay'; } return $enabled_methods; } /** * Gets settings that are shared between the Express Checkout button and the WooPay button. * * @return array */ public function get_common_button_settings() { $button_type = $this->gateway->get_option( 'payment_request_button_type' ); return [ 'type' => $button_type, 'theme' => $this->gateway->get_option( 'payment_request_button_theme' ), 'height' => $this->get_button_height(), 'radius' => $this->gateway->get_option( 'payment_request_button_border_radius' ), ]; } /** * Gets the context for where the button is being displayed. * * @return string */ public function get_button_context() { if ( $this->is_product() ) { return 'product'; } if ( $this->is_cart() ) { return 'cart'; } if ( $this->is_pay_for_order_page() ) { return 'pay_for_order'; } if ( $this->is_checkout() ) { return 'checkout'; } return ''; } /** * Gets the button height. * * @return string */ public function get_button_height() { $height = $this->gateway->get_option( 'payment_request_button_size' ); if ( 'medium' === $height ) { return '48'; } if ( 'large' === $height ) { return '55'; } // for the "default"/"small" and "catch-all" scenarios. return '40'; } /** * Get product from product page or product_page shortcode. * * @return WC_Product|false|null Product object. */ public function get_product() { global $post; if ( is_product() ) { return wc_get_product( $post->ID ); } if ( wc_post_content_has_shortcode( 'product_page' ) ) { // Get id from product_page shortcode. preg_match( '/\[product_page id="(?<id>\d+)"\]/', $post->post_content, $shortcode_match ); if ( isset( $shortcode_match['id'] ) ) { return wc_get_product( $shortcode_match['id'] ); } } return null; } /** * Used to get the order in admin edit page. * * @return WC_Order|WC_Order_Refund|bool */ public function get_current_order() { global $theorder; global $post; if ( is_object( $theorder ) ) { return $theorder; } if ( is_object( $post ) ) { return wc_get_order( $post->ID ); } return false; } /** * Returns true if the provided WC_Product is a subscription, false otherwise. * * @param WC_Product $product The product to check. * * @return bool True if product is subscription, false otherwise. */ public function is_product_subscription( WC_Product $product ): bool { return 'subscription' === $product->get_type() || 'subscription_variation' === $product->get_type() || 'variable-subscription' === $product->get_type(); } /** * Checks whether Express Checkout Element Button should be available on this page. * * @return bool */ public function should_show_express_checkout_button() { // If account is not connected, then bail. if ( ! $this->account->is_stripe_connected( false ) ) { return false; } // If no SSL, bail. if ( ! WC_Payments::mode()->is_test() && ! is_ssl() ) { Logger::log( 'Stripe Express Checkout live mode requires SSL.' ); return false; } // Page not supported. if ( ! $this->is_product() && ! $this->is_cart() && ! $this->is_checkout() ) { return false; } // Product page, but no express checkout methods available in settings. if ( $this->is_product() && ! $this->is_any_express_checkout_method_enabled_at( 'product' ) ) { return false; } // Checkout page, but no express checkout methods available in settings. if ( $this->is_checkout() && ! $this->is_any_express_checkout_method_enabled_at( 'checkout' ) ) { return false; } // Cart page, but no express checkout methods available in settings. if ( $this->is_cart() && ! $this->is_any_express_checkout_method_enabled_at( 'cart' ) ) { return false; } // Product page, but has unsupported product type. if ( $this->is_product() && ! $this->is_product_supported() ) { Logger::log( 'Product page has unsupported product type ( Express Checkout Element button disabled )' ); return false; } // Cart has unsupported product type. if ( ( $this->is_checkout() || $this->is_cart() ) && ! $this->has_allowed_items_in_cart() ) { Logger::log( 'Items in the cart have unsupported product type ( Express Checkout Element button disabled )' ); return false; } // Order total doesn't matter for Pay for Order page. Thus, this page should always display payment buttons. if ( $this->is_pay_for_order_page() ) { return $this->is_pay_for_order_supported(); } // Non-shipping product and tax is calculated based on shopper billing address. Excludes Pay for Order page. if ( // If the product doesn't needs shipping. ( // on the product page. ( $this->is_product() && ! $this->product_needs_shipping( $this->get_product() ) ) || // on the cart or checkout page. ( ( $this->is_cart() || $this->is_checkout() ) && ! WC()->cart->needs_shipping() ) ) // ...and tax is calculated based on billing address. && wc_tax_enabled() && 'billing' === get_option( 'woocommerce_tax_based_on' ) && 'yes' !== get_option( 'woocommerce_prices_include_tax' ) ) { return false; } // Cart total is 0 or is on product page and product price is 0. // Exclude pay-for-order pages from this check. if ( ( ! $this->is_product() && ! $this->is_pay_for_order_page() && 0.0 === (float) WC()->cart->get_total( 'edit' ) ) || ( $this->is_product() && 0.0 === (float) $this->get_product()->get_price() ) ) { Logger::log( 'Order price is 0 ( Express Checkout Element button disabled )' ); return false; } return true; } /** * Check if the passed product needs to be shipped. * * @param WC_Product $product The product to check. * * @return bool Returns true if the product requires shipping; otherwise, returns false. */ public function product_needs_shipping( WC_Product $product ) { if ( ! $product ) { return false; } return wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping(); } /** * Checks to make sure product type is supported. * * @return array */ public function supported_product_types() { return apply_filters( 'wcpay_payment_request_supported_types', [ 'simple', 'variable', 'variation', 'subscription', 'variable-subscription', 'subscription_variation', 'booking', 'bundle', 'composite', 'mix-and-match', ] ); } /** * Checks the cart to see if all items are allowed to be used. * * @return boolean */ public function has_allowed_items_in_cart() { /** * Pre Orders compatbility where we don't support charge upon release. */ if ( class_exists( 'WC_Pre_Orders_Cart' ) && WC_Pre_Orders_Cart::cart_contains_pre_order() && class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) ) { return false; } foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); if ( ! in_array( $_product->get_type(), $this->supported_product_types(), true ) ) { return false; } /** * Filter whether product supports Express Checkout Element Button on cart page. * * @since 6.9.0 * * @param boolean $is_supported Whether product supports Express Checkout Element Button on cart page. * @param object $_product Product object. */ if ( ! apply_filters( 'wcpay_payment_request_is_cart_supported', true, $_product ) ) { return false; } /** * Trial subscriptions with shipping are not supported. */ if ( class_exists( 'WC_Subscriptions_Product' ) && WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) { return false; } } // We don't support multiple packages with Express Checkout Element Buttons because we can't offer a good UX. $packages = WC()->cart->get_shipping_packages(); if ( 1 < ( is_countable( $packages ) ? count( $packages ) : 0 ) ) { return false; } return true; } /** * Gets the product data for the currently viewed page. * * @return mixed Returns false if not on a product page, the product information otherwise. */ public function get_product_data() { if ( ! $this->is_product() ) { return false; } /** @var WC_Product_Variable $product */ // phpcs:ignore $product = $this->get_product(); $currency = get_woocommerce_currency(); if ( 'variable' === $product->get_type() || 'variable-subscription' === $product->get_type() ) { $variation_attributes = $product->get_variation_attributes(); $attributes = []; foreach ( $variation_attributes as $attribute_name => $attribute_values ) { $attribute_key = 'attribute_' . sanitize_title( $attribute_name ); // Passed value via GET takes precedence. Otherwise get the default value for given attribute. $attributes[ $attribute_key ] = isset( $_GET[ $attribute_key ] ) // phpcs:ignore WordPress.Security.NonceVerification ? wc_clean( wp_unslash( $_GET[ $attribute_key ] ) ) // phpcs:ignore WordPress.Security.NonceVerification : $product->get_variation_default_attribute( $attribute_name ); } $data_store = WC_Data_Store::load( 'product' ); $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); if ( ! empty( $variation_id ) ) { $product = wc_get_product( $variation_id ); } } try { $price = $this->get_product_price( $product ); } catch ( Invalid_Price_Exception $e ) { Logger::log( $e->getMessage() ); return false; } $data = []; $items = []; $items[] = [ 'label' => $product->get_name(), 'amount' => WC_Payments_Utils::prepare_amount( $price, $currency ), ]; $total_tax = 0; foreach ( $this->get_taxes_like_cart( $product, $price ) as $tax ) { $total_tax += $tax; $items[] = [ 'label' => __( 'Tax', 'woocommerce-payments' ), 'amount' => WC_Payments_Utils::prepare_amount( $tax, $currency ), 'pending' => 0 === $tax, ]; } if ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() ) { $items[] = [ 'label' => __( 'Shipping', 'woocommerce-payments' ), 'amount' => 0, 'pending' => true, ]; $data['shippingOptions'] = [ 'id' => 'pending', 'label' => __( 'Pending', 'woocommerce-payments' ), 'detail' => '', 'amount' => 0, ]; } $data['displayItems'] = $items; $data['total'] = [ 'label' => apply_filters( 'wcpay_payment_request_total_label', $this->get_total_label() ), 'amount' => WC_Payments_Utils::prepare_amount( $price + $total_tax, $currency ), 'pending' => true, ]; $data['needs_shipping'] = ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() ); $data['currency'] = strtolower( $currency ); $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 ); $data['product_type'] = $product->get_type(); return apply_filters( 'wcpay_payment_request_product_data', $data, $product ); } /** * The Store API doesn't allow checkout without the billing email address present on the order data. * https://github.com/woocommerce/woocommerce/issues/48540 * * @return bool */ private function is_pay_for_order_supported() { $order_id = absint( get_query_var( 'order-pay' ) ); if ( 0 === $order_id ) { return false; } $order = wc_get_order( $order_id ); if ( ! is_a( $order, 'WC_Order' ) ) { return false; } // we don't need to check its validity or value, we just need to ensure a billing email is present. $billing_email = $order->get_billing_email(); if ( ! empty( $billing_email ) ) { return true; } Logger::log( 'Billing email not present ( Express Checkout Element button disabled )' ); return false; } /** * Whether product page has a supported product. * * @return boolean */ private function is_product_supported() { $product = $this->get_product(); $is_supported = true; if ( is_null( $product ) || ! is_object( $product ) ) { $is_supported = false; } else { // Simple subscription that needs shipping with free trials is not supported. $is_free_trial_simple_subs = class_exists( 'WC_Subscriptions_Product' ) && $product->get_type() === 'subscription' && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0; if ( ! in_array( $product->get_type(), $this->supported_product_types(), true ) || $is_free_trial_simple_subs || ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) // Pre Orders charge upon release not supported. || ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) // Composite products are not supported on the product page. || ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) // Mix and match products are not supported on the product page. ) { $is_supported = false; } elseif ( class_exists( 'WC_Product_Addons_Helper' ) ) { // File upload addon not supported. $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() ); foreach ( $product_addons as $addon ) { if ( 'file_upload' === $addon['type'] ) { $is_supported = false; break; } } } } return apply_filters( 'wcpay_payment_request_is_product_supported', $is_supported, $product ); } /** * Gets the product total price. * * @param object $product WC_Product_* object. * @param bool $is_deposit Whether customer is paying a deposit. * @param int $deposit_plan_id The ID of the deposit plan. * @return mixed Total price. * * @throws Invalid_Price_Exception Whenever a product has no price. */ public function get_product_price( $product, ?bool $is_deposit = null, int $deposit_plan_id = 0 ) { // If prices should include tax, using tax inclusive price. if ( $this->cart_prices_include_tax() ) { $base_price = wc_get_price_including_tax( $product ); } else { $base_price = wc_get_price_excluding_tax( $product ); } // If WooCommerce Deposits is active, we need to get the correct price for the product. if ( class_exists( 'WC_Deposits_Product_Manager' ) && class_exists( 'WC_Deposits_Plans_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) { // If is_deposit is null, we use the default deposit type for the product. if ( is_null( $is_deposit ) ) { $is_deposit = 'deposit' === WC_Deposits_Product_Manager::get_deposit_selected_type( $product->get_id() ); } if ( $is_deposit ) { $deposit_type = WC_Deposits_Product_Manager::get_deposit_type( $product->get_id() ); $available_plan_ids = WC_Deposits_Plans_Manager::get_plan_ids_for_product( $product->get_id() ); // Default to first (default) plan if no plan is specified. if ( 'plan' === $deposit_type && 0 === $deposit_plan_id && ! empty( $available_plan_ids ) ) { $deposit_plan_id = $available_plan_ids[0]; } // Ensure the selected plan is available for the product. if ( 0 === $deposit_plan_id || in_array( $deposit_plan_id, $available_plan_ids, true ) ) { $base_price = WC_Deposits_Product_Manager::get_deposit_amount( $product, $deposit_plan_id, 'display', $base_price ); } } } // Add subscription sign-up fees to product price. $sign_up_fee = 0; $subscription_types = [ 'subscription', 'subscription_variation', ]; if ( in_array( $product->get_type(), $subscription_types, true ) && class_exists( 'WC_Subscriptions_Product' ) ) { // When there is no sign-up fee, `get_sign_up_fee` falls back to an int 0. $sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product ); } if ( ! is_numeric( $base_price ) || ! is_numeric( $sign_up_fee ) ) { $error_message = sprintf( // Translators: %d is the numeric ID of the product without a price. __( 'Express checkout does not support products without prices! Please add a price to product #%d', 'woocommerce-payments' ), (int) $product->get_id() ); throw new Invalid_Price_Exception( esc_html( $error_message ) ); } return $base_price + $sign_up_fee; } /** * Calculates taxes as displayed on cart, based on a product and a particular price. * * @param WC_Product $product The product, for retrieval of tax classes. * @param float $price The price, which to calculate taxes for. * @return array An array of final taxes. */ public function get_taxes_like_cart( $product, $price ) { if ( ! wc_tax_enabled() || $this->cart_prices_include_tax() ) { // Only proceed when taxes are enabled, but not included. return []; } // Follows the way `WC_Cart_Totals::get_item_tax_rates()` works. $tax_class = $product->get_tax_class(); $rates = WC_Tax::get_rates( $tax_class ); // No cart item, `woocommerce_cart_totals_get_item_tax_rates` can't be applied here. // Normally there should be a single tax, but `calc_tax` returns an array, let's use it. return WC_Tax::calc_tax( $price, $rates, false ); } /** * Sanitize string for comparison. * * @param string $string String to be sanitized. * * @return string The sanitized string. */ public function sanitize_string( $string ) { return trim( wc_strtolower( remove_accents( $string ) ) ); } } includes/express-checkout/class-wc-payments-express-checkout-button-handler.php 0000777 00000037550 15251156605 0024176 0 ustar 00 <?php /** * Class WC_Payments_Express_Checkout_Button_Handler * Adds support for Apple Pay, Google Pay and ECE API buttons. * Utilizes the Stripe Express Checkout Element to support checkout from the product detail and cart pages. * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; } use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use WCPay\Fraud_Prevention\Fraud_Prevention_Service; /** * WC_Payments_Express_Checkout_Button_Handler class. */ class WC_Payments_Express_Checkout_Button_Handler { const DEFAULT_BORDER_RADIUS_IN_PX = 4; /** * WC_Payments_Account instance to get information about the account * * @var WC_Payments_Account */ private $account; /** * WC_Payment_Gateway_WCPay instance. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * Express Checkout Ajax Handle instance. * * @var WC_Payments_Express_Checkout_Button_Helper */ private $express_checkout_helper; /** * Express Checkout Helper instance. * * @var WC_Payments_Express_Checkout_Ajax_Handler */ private $express_checkout_ajax_handler; /** * Initialize class actions. * * @param WC_Payments_Account $account Account information. * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. * @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper Express checkout helper. * @param WC_Payments_Express_Checkout_Ajax_Handler $express_checkout_ajax_handler Express checkout ajax handler. */ public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper, WC_Payments_Express_Checkout_Ajax_Handler $express_checkout_ajax_handler ) { $this->account = $account; $this->gateway = $gateway; $this->express_checkout_helper = $express_checkout_helper; $this->express_checkout_ajax_handler = $express_checkout_ajax_handler; } /** * Initialize hooks. * * @return void */ public function init() { // Checks if WCPay is enabled. if ( ! $this->gateway->is_enabled() ) { return; } // Checks if at least one express checkout method is enabled. if ( ! $this->gateway->is_payment_request_enabled() && ! $this->express_checkout_helper->can_use_amazon_pay() ) { return; } // Don't load for change payment method page. if ( isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return; } add_action( 'template_redirect', [ $this, 'set_session' ] ); add_action( 'wcpay_payment_fields_js_config', [ $this, 'payment_fields_js_config' ] ); add_action( 'template_redirect', [ $this, 'handle_express_checkout_redirect' ] ); add_filter( 'woocommerce_login_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 ); add_filter( 'woocommerce_registration_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 ); add_filter( 'woocommerce_cart_needs_shipping_address', [ $this, 'filter_cart_needs_shipping_address' ], 11, 1 ); add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] ); add_filter( 'woocommerce_gateway_title', [ $this, 'filter_gateway_title' ], 10, 2 ); $this->express_checkout_ajax_handler->init(); if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) { $this->register_ece_data_for_block_editor(); } } /** * Appends express-checkout-related data to the JS configuration used during checkout. * * @param array $config The configuration to be provided to the JS. * * @return mixed */ public function payment_fields_js_config( $config ) { $config['isPaymentRequestEnabled'] = $this->gateway->is_payment_request_enabled() && $this->express_checkout_helper->is_express_checkout_method_enabled_at( 'checkout', 'payment_request' ); $config['isAmazonPayEnabled'] = $this->express_checkout_helper->can_use_amazon_pay() && $this->express_checkout_helper->is_express_checkout_method_enabled_at( 'checkout', 'amazon_pay' ); return $config; } /** * The settings for the `button` attribute - they depend on the "grouped settings" flag value. * * @return array */ public function get_button_settings() { $button_type = $this->gateway->get_option( 'payment_request_button_type' ); $common_settings = $this->express_checkout_helper->get_common_button_settings(); $express_checkout_button_settings = [ // Default format is en_US. 'locale' => apply_filters( 'wcpay_payment_request_button_locale', substr( get_locale(), 0, 2 ) ), 'branded_type' => 'default' === $button_type ? 'short' : 'long', ]; return array_merge( $common_settings, $express_checkout_button_settings ); } /** * Settings array for the user authentication dialog and redirection. * * @return array|false */ public function get_login_confirmation_settings() { if ( is_user_logged_in() || ! $this->is_authentication_required() ) { return false; } /* translators: The text encapsulated in `**` can be replaced with "Apple Pay" or "Google Pay". Please translate this text, but don't remove the `**`. */ $message = __( 'To complete your transaction with **the selected payment method**, you must log in or create an account with our site.', 'woocommerce-payments' ); $redirect_url = add_query_arg( [ '_wpnonce' => wp_create_nonce( 'wcpay-set-redirect-url' ), 'wcpay_express_checkout_redirect_url' => rawurlencode( home_url( add_query_arg( [] ) ) ), // Current URL to redirect to after login. ], home_url() ); return [ // nosemgrep: audit.php.wp.security.xss.query-arg -- home_url passed in to add_query_arg. 'message' => $message, 'redirect_url' => $redirect_url, ]; } /** * Checks whether authentication is required for checkout. * * @return bool */ public function is_authentication_required() { // If guest checkout is disabled and account creation is not possible, authentication is required. if ( 'no' === get_option( 'woocommerce_enable_guest_checkout', 'yes' ) && ! $this->is_account_creation_possible() ) { return true; } // If cart contains subscription and account creation is not posible, authentication is required. if ( $this->has_subscription_product() && ! $this->is_account_creation_possible() ) { return true; } return false; } /** * Checks whether cart contains a subscription product or this is a subscription product page. * * @return boolean */ public function has_subscription_product() { if ( ! class_exists( 'WC_Subscriptions_Product' ) || ! class_exists( 'WC_Subscriptions_Cart' ) ) { return false; } if ( $this->express_checkout_helper->is_product() ) { $product = $this->express_checkout_helper->get_product(); if ( WC_Subscriptions_Product::is_subscription( $product ) ) { return true; } } if ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) { if ( WC_Subscriptions_Cart::cart_contains_subscription() ) { return true; } } return false; } /** * Checks whether account creation is possible during checkout. * * @return bool */ public function is_account_creation_possible() { $is_signup_from_checkout_allowed = 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'no' ); // If a subscription is being purchased, check if account creation is allowed for subscriptions. if ( ! $is_signup_from_checkout_allowed && $this->has_subscription_product() ) { $is_signup_from_checkout_allowed = 'yes' === get_option( 'woocommerce_enable_signup_from_checkout_for_subscriptions', 'no' ); } // If automatically generate username/password are disabled, the Express Checkout API // can't include any of those fields, so account creation is not possible. return ( $is_signup_from_checkout_allowed && 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && 'yes' === get_option( 'woocommerce_registration_generate_password', 'yes' ) ); } /** * Gets the parameters needed for Express Checkout functionality. * * @return array Parameters for Express Checkout. */ public function get_express_checkout_params() { /** * Allowing some specific configuration to be tweaked by 3pd. * * @since 9.5.0 */ return array_merge( apply_filters( 'wcpay_express_checkout_js_params', [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), 'nonce' => [ 'platform_tracker' => wp_create_nonce( 'platform_tracks_nonce' ), // needed to communicate via the Store API. 'tokenized_cart_nonce' => wp_create_nonce( 'woopayments_tokenized_cart_nonce' ), 'tokenized_cart_session_nonce' => wp_create_nonce( 'woopayments_tokenized_cart_session_nonce' ), 'store_api_nonce' => wp_create_nonce( 'wc_store_api' ), ], 'checkout' => [ 'currency_code' => strtolower( get_woocommerce_currency() ), 'currency_decimals' => WC_Payments::get_localization_service()->get_currency_format( get_woocommerce_currency() )['num_decimals'], 'country_code' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ), 'needs_shipping' => WC()->cart->needs_shipping(), // Defaults to 'required' to match how core initializes this option. 'needs_payer_phone' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), 'allowed_shipping_countries' => array_keys( WC()->countries->get_shipping_countries() ?? [] ), 'display_prices_with_tax' => 'incl' === get_option( 'woocommerce_tax_display_cart' ), ], 'has_subscription' => $this->has_subscription_product(), 'button' => $this->get_button_settings(), 'login_confirmation' => $this->get_login_confirmation_settings(), 'button_context' => $this->express_checkout_helper->get_button_context(), 'has_block' => has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ), 'product' => $this->express_checkout_helper->get_product_data(), 'store_name' => get_bloginfo( 'name' ), 'enabled_methods' => $this->express_checkout_helper->get_enabled_express_checkout_methods_for_context(), ] ), [ // placing these outside of the filter to prevent modification of the values. 'stripe' => [ 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ), 'accountId' => $this->account->get_stripe_account_id(), 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), ], 'flags' => [ 'isEceUsingConfirmationTokens' => WC_Payments_Features::is_ece_confirmation_tokens_enabled(), ], ] ); } /** * Load public scripts and styles. */ public function scripts() { // Don't load scripts if page is not supported. if ( ! $this->express_checkout_helper->should_show_express_checkout_button() ) { return; } $express_checkout_params = $this->get_express_checkout_params(); WC_Payments::register_script_with_dependencies( 'WCPAY_EXPRESS_CHECKOUT_ECE', 'dist/express-checkout', [ 'jquery', 'stripe', ] ); WC_Payments_Utils::enqueue_style( 'WCPAY_EXPRESS_CHECKOUT_ECE', plugins_url( 'dist/express-checkout.css', WCPAY_PLUGIN_FILE ), [], WC_Payments::get_file_version( 'dist/express-checkout.css' ) ); wp_localize_script( 'WCPAY_EXPRESS_CHECKOUT_ECE', 'wcpayExpressCheckoutParams', $express_checkout_params ); wp_localize_script( 'WCPAY_BLOCKS_CHECKOUT', 'wcpayExpressCheckoutParams', $express_checkout_params ); wp_set_script_translations( 'WCPAY_EXPRESS_CHECKOUT_ECE', 'woocommerce-payments' ); wp_enqueue_script( 'WCPAY_EXPRESS_CHECKOUT_ECE' ); Fraud_Prevention_Service::maybe_append_fraud_prevention_token(); $gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( isset( $gateways['woocommerce_payments'] ) ) { WC_Payments::get_wc_payments_checkout()->register_scripts(); } } /** * Display the express checkout button. */ public function display_express_checkout_button_html() { if ( ! $this->express_checkout_helper->should_show_express_checkout_button() ) { return; } ?> <div id="wcpay-express-checkout-element"></div> <?php } /** * Sets the WC customer session if one is not set. * This is needed so nonces can be verified by AJAX Request. * * @return void */ public function set_session() { // Skip if there's already an active WC session. Otherwise, only set session cookies on checkout and cart pages. // This helps with caching as product pages can be cached when no cookies are present. $has_active_session = isset( WC()->session ) && WC()->session->has_session(); if ( $has_active_session ) { return; } $is_checkout_or_cart = $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart(); if ( ! $is_checkout_or_cart ) { return; } WC()->session->set_customer_session_cookie( true ); } /** * Handles express checkout redirect when the redirect dialog "Continue" button is clicked. */ public function handle_express_checkout_redirect() { if ( ! empty( $_GET['wcpay_express_checkout_redirect_url'] ) && ! empty( $_GET['_wpnonce'] ) && wp_verify_nonce( $_GET['_wpnonce'], 'wcpay-set-redirect-url' ) // @codingStandardsIgnoreLine ) { $url = rawurldecode( esc_url_raw( wp_unslash( $_GET['wcpay_express_checkout_redirect_url'] ) ) ); // Sets a redirect URL cookie for 10 minutes, which we will redirect to after authentication. // Users will have a 10 minute timeout to login/create account, otherwise redirect URL expires. wc_setcookie( 'wcpay_express_checkout_redirect_url', $url, time() + MINUTE_IN_SECONDS * 10 ); // Redirects to "my-account" page. wp_safe_redirect( get_permalink( get_option( 'woocommerce_myaccount_page_id' ) ) ); } } /** * Returns the login redirect URL. * * @param string $redirect Default redirect URL. * * @return string Redirect URL. */ public function get_login_redirect_url( $redirect ) { $url = esc_url_raw( wp_unslash( $_COOKIE['wcpay_express_checkout_redirect_url'] ?? '' ) ); if ( empty( $url ) ) { return $redirect; } wc_setcookie( 'wcpay_express_checkout_redirect_url', '' ); return $url; } /** * Determine whether to filter the cart needs shipping address. * * @param boolean $needs_shipping_address Whether the cart needs a shipping address. */ public function filter_cart_needs_shipping_address( $needs_shipping_address ) { if ( $this->has_subscription_product() && wc_get_shipping_method_count( true, true ) === 0 ) { return false; } return $needs_shipping_address; } /** * Filters the gateway title to reflect the button type used. * * @param string $title Gateway title. * @param string $id Gateway ID. */ public function filter_gateway_title( $title, $id ) { if ( 'woocommerce_payments' !== $id || ! is_admin() ) { return $title; } $order = $this->express_checkout_helper->get_current_order(); $method_title = is_object( $order ) ? $order->get_payment_method_title() : ''; if ( ! empty( $method_title ) ) { if ( strpos( $method_title, 'Apple Pay' ) === 0 || strpos( $method_title, 'Google Pay' ) === 0 || strpos( $method_title, 'Payment Request' ) === 0 // Legacy PRB title. ) { return $method_title; } } return $title; } /** * Add ECE data to `wcSettings` to allow it to be accessed by the front-end JS script in the Block editor. * * @return void */ private function register_ece_data_for_block_editor() { $data_registry = Package::container()->get( AssetDataRegistry::class ); if ( $data_registry->exists( 'ece_data' ) ) { return; } $data_registry->add( 'ece_data', [ 'button' => $this->get_button_settings(), ] ); } } includes/notes/class-wc-payments-remote-note-service.php 0000777 00000007417 15251156605 0017505 0 ustar 00 <?php /** * WooCommerce inbox remote note service. * * @package WooCommerce\Payments\Admin */ use Automattic\WooCommerce\Admin\Notes\Note; use WCPay\Exceptions\Rest_Request_Exception; defined( 'ABSPATH' ) || exit; /** * Class WC_Payments_Remote_Note_Service */ class WC_Payments_Remote_Note_Service { const NOTE_NAME_PREFIX = 'wc-payments-remote-notes-'; /** * Notes data store. * * @var WC_Data_Store */ private $note_data_store; /** * Class constructor. * * @param WC_Data_Store $note_data_store WC Admin note data store. */ public function __construct( WC_Data_Store $note_data_store ) { $this->note_data_store = $note_data_store; } /** * Puts the given note data in the inbox if it hasn't been added before. * * @param array $note_data Note data from the API. * * @return bool True if the note has been added. * * @throws Rest_Request_Exception If note data is invalid. */ public function put_note( array $note_data ): bool { $note = $this->create_note( $note_data ); if ( ! $this->can_note_be_added( $note->get_name() ) ) { return false; } $this->note_data_store->create( $note ); return true; } /** * Creates a new instance of a note from API note data. * * @param array $note_data The note data to process. * * @return Automattic\WooCommerce\Admin\Notes\WC_Admin_Note|Automattic\WooCommerce\Admin\Notes\Note Note object. * * @throws Rest_Request_Exception If note data is invalid. */ private function create_note( array $note_data ) { if ( ! isset( $note_data['title'], $note_data['content'] ) ) { throw new Rest_Request_Exception( 'Invalid note.' ); } $title = $note_data['title']; $content = $note_data['content']; $note_name = self::NOTE_NAME_PREFIX . ( $note_data['name'] ?? md5( $title . $content ) ); $note = new Note(); $note->set_title( $title ); $note->set_content( $content ); $note->set_content_data( (object) [] ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( $note_name ); $note->set_source( 'woocommerce-payments' ); if ( isset( $note_data['actions'] ) && is_array( $note_data['actions'] ) ) { foreach ( $note_data['actions'] as $action_key => $action ) { if ( ! isset( $action['label'], $action['url'] ) ) { throw new Rest_Request_Exception( 'Invalid note.' ); } if ( 'wcpay_settings' === $action['url'] ) { $url = WC_Payments_Admin_Settings::get_settings_url(); } elseif ( isset( $action['url_is_admin'] ) && (bool) $action['url_is_admin'] ) { $url = admin_url( $action['url'] ); } else { throw new Rest_Request_Exception( 'Invalid note.' ); } $note->add_action( $note_name . '-' . $action_key, $action['label'], $url, $action['status'] ?? Note::E_WC_ADMIN_NOTE_ACTIONED, $action['primary'] ?? false ); } } return $note; } /** * Deletes all of the notes where names are prefixed with the value of the NOTE_NAME_PREFIX constant. * * @return void */ public function delete_notes() { global $wpdb; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}wc_admin_note_actions WHERE name LIKE %s", $wpdb->esc_like( self::NOTE_NAME_PREFIX ) . '%' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}wc_admin_notes WHERE name LIKE %s", $wpdb->esc_like( self::NOTE_NAME_PREFIX ) . '%' ) ); } /** * Checks if the note can be added. * * @param string $note_name Name of the note to add. * * @return boolean True if the note can be added. */ private function can_note_be_added( string $note_name ): bool { $note_ids = $this->note_data_store->get_notes_with_name( $note_name ); return empty( $note_ids ); } } includes/notes/class-wc-payments-notes-stripe-billing-deprecation.php 0000777 00000004502 15251156605 0022146 0 ustar 00 <?php /** * Set up Stripe Billing deprecation note for WooCommerce inbox. * * @package WooCommerce\Payments\Admin */ use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; defined( 'ABSPATH' ) || exit; /** * Class WC_Payments_Notes_Stripe_Billing_Deprecation */ class WC_Payments_Notes_Stripe_Billing_Deprecation { use NoteTraits { can_be_added as protected trait_can_be_added; } /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-payments-notes-stripe-billing-deprecation'; /** * URL to the WooCommerce Subscriptions plugin page. */ const NOTE_SUBSCRIPTIONS_URL = 'https://woocommerce.com/products/woocommerce-subscriptions/'; /** * Checks if a note can and should be added. * * @return bool */ public static function can_be_added() { // Only show if Stripe Billing is enabled and WooCommerce Subscriptions is not installed. if ( ! self::is_bundled_subscriptions_enabled() ) { return false; } // If wcpay version is > 10.2.99 bail to not show the notice indefinitely. if ( version_compare( WC_Payments::get_file_version( WCPAY_PLUGIN_FILE ), '10.2.99', '>' ) ) { return false; } return self::trait_can_be_added(); } /** * Get the note. */ public static function get_note() { $note = new Note(); $note->set_title( __( 'Built-in subscriptions functionality has been removed. Here\'s what to do', 'woocommerce-payments' ) ); $note->set_content( __( 'To continue offering subscriptions and gain access to your data, please install WooCommerce Subscriptions. WooPayments no longer supports this feature.', 'woocommerce-payments' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-payments' ); $note->add_action( 'get-woocommerce-subscriptions', __( 'Install WooCommerce Subscriptions', 'woocommerce-payments' ), self::NOTE_SUBSCRIPTIONS_URL ); return $note; } /** * Check if bundled subscriptions are enabled. * * @return bool */ protected static function is_bundled_subscriptions_enabled() { $has_bundled_subs = WC_Payments_Features::is_wcpay_subscriptions_enabled() || WC_Payments_Features::is_stripe_billing_enabled(); $has_wc_subs = class_exists( 'WC_Subscriptions' ); return $has_bundled_subs && ! $has_wc_subs; } } includes/notes/class-wc-payments-notes-canceled-auth-remediation.php 0000777 00000007416 15251156605 0021731 0 ustar 00 <?php /** * WC Admin Inbox note for canceled authorization fee remediation. * * @package WooCommerce\Payments\Admin */ use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; defined( 'ABSPATH' ) || exit; /** * Class WC_Payments_Notes_Canceled_Auth_Remediation * * Displays an inbox note to merchants who may be affected by the canceled * authorization analytics bug, prompting them to run the remediation tool. */ class WC_Payments_Notes_Canceled_Auth_Remediation { use NoteTraits { can_be_added as protected trait_can_be_added; } /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-payments-notes-canceled-auth-remediation'; /** * URL to the WooCommerce Tools page. */ const NOTE_TOOLS_URL = 'admin.php?page=wc-status&tab=tools'; /** * Checks if a note can and should be added. * * @return bool */ public static function can_be_added() { include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php'; // Don't show if remediation is already complete. if ( 'completed' === get_option( 'wcpay_fee_remediation_status', '' ) ) { return false; } // Don't show if remediation is already running. if ( self::is_remediation_running() ) { return false; } // Only show if there are affected orders. if ( ! self::has_affected_orders() ) { return false; } return self::trait_can_be_added(); } /** * Get the note. * * @return Note */ public static function get_note() { $note = new Note(); $note->set_title( __( 'WooPayments: Fix incorrect order data', 'woocommerce-payments' ) ); $note->set_content( __( 'Some orders with canceled payment authorizations have incorrect data that may cause negative values in your WooCommerce Analytics. This affects stores using manual capture (authorize and capture separately). Run the fix tool to correct this.', 'woocommerce-payments' ) ); $note->set_content_data( (object) [] ); $note->set_type( Note::E_WC_ADMIN_NOTE_WARNING ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-payments' ); $note->add_action( 'run-remediation-tool', __( 'Go to Tools page', 'woocommerce-payments' ), admin_url( self::NOTE_TOOLS_URL ), 'actioned', false ); return $note; } /** * Check if there are orders that need remediation. * * Uses a state machine backed by an option to avoid running the expensive * query inline. On the first call, schedules an async Action Scheduler job * and returns false. The note will be added on a subsequent admin_init * once the async check completes. * * @return bool */ private static function has_affected_orders() { $state = get_option( WC_Payments_Remediate_Canceled_Auth_Fees::CHECK_STATE_OPTION_KEY ); if ( false === $state ) { self::schedule_check(); return false; } if ( 'has_affected_orders' === $state ) { return true; } // 'scheduled', 'no_affected_orders', or any unexpected value. return false; } /** * Schedule the async affected orders check via Action Scheduler. * * @return void */ private static function schedule_check() { if ( ! function_exists( 'as_schedule_single_action' ) ) { return; } update_option( WC_Payments_Remediate_Canceled_Auth_Fees::CHECK_STATE_OPTION_KEY, 'scheduled', true ); as_schedule_single_action( time() + 10, WC_Payments_Remediate_Canceled_Auth_Fees::CHECK_AFFECTED_ORDERS_HOOK, [], 'woocommerce-payments' ); } /** * Check if remediation is currently running. * * @return bool */ private static function is_remediation_running() { if ( ! function_exists( 'as_has_scheduled_action' ) ) { return false; } return as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK ); } } includes/notes/class-wc-payments-notes-instant-deposits-eligible.php 0000777 00000003301 15251156605 0022005 0 ustar 00 <?php /** * Notify merchant that they are eligible for Instant Deposits. * * @package WooCommerce\Payments\Admin */ use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; defined( 'ABSPATH' ) || exit; /** * Class WC_Payments_Notes_Instant_Deposits_Eligible */ class WC_Payments_Notes_Instant_Deposits_Eligible { use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-payments-notes-instant-deposits-eligible'; /** * Get the note. */ public static function get_note() { $note = new Note(); $note->set_title( sprintf( /* translators: %s: WooPayments */ __( 'You’re now eligible to receive Instant Payouts with %s', 'woocommerce-payments' ), 'WooPayments' ) ); $note->set_content( WC_Payments_Utils::esc_interpolated_html( sprintf( /* translators: %s: WooPayments */ __( "Get immediate access to your funds when you need them – including nights, weekends, and holidays. With %s' <a>Instant Payouts feature</a>, you're able to transfer your earnings to a debit card within minutes.", 'woocommerce-payments' ), 'WooPayments' ), [ 'a' => '<a href="https://woocommerce.com/document/woopayments/payouts/instant-payouts/">' ] ) ); $note->set_content_data( (object) [] ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-payments' ); $note->add_action( self::NOTE_NAME, __( 'Request an instant payout', 'woocommerce-payments' ), 'https://woocommerce.com/document/woopayments/payouts/instant-payouts/#request-an-instant-payout', 'unactioned', true ); return $note; } } includes/notes/class-wc-payments-notes-qualitative-feedback.php 0000777 00000004144 15251156605 0021003 0 ustar 00 <?php /** * Qualitative feedback inbox note for accounts that have been active for 60 days and have at least one transaction. * * @package WooCommerce\Payments\Admin */ use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; defined( 'ABSPATH' ) || exit; /** * Class WC_Payments_Notes_Qualitative_Feedback */ class WC_Payments_Notes_Qualitative_Feedback { use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-payments-notes-qualitative-feedback'; /** * Name of the note for use in the database. */ const NOTE_DOCUMENTATION_URL = 'http://automattic.survey.fm/wc-pay-existing'; /** * Get the note. */ public static function get_note() { global $wpdb; // Store must be at least 60 days old. if ( ! self::wc_admin_active_for( 60 * DAY_IN_SECONDS ) ) { return; } // We should have at least one transaction. if ( WC_Payments_Utils::is_hpos_tables_usage_enabled() ) { $result = $wpdb->get_var( "SELECT EXISTS( SELECT 1 FROM {$wpdb->prefix}wc_orders_meta WHERE meta_key = '_wcpay_transaction_fee' LIMIT 1) AS count;" ); } else { $result = $wpdb->get_var( "SELECT EXISTS( SELECT 1 FROM {$wpdb->postmeta} WHERE meta_key = '_wcpay_transaction_fee' LIMIT 1) AS count;" ); } if ( 1 !== intval( $result ) ) { return; } $note = new Note(); $note->set_title( sprintf( /* translators: %s: WooPayments */ __( 'Help us make improvements to %s', 'woocommerce-payments' ), 'WooPayments' ) ); $note->set_content( __( 'Share your feedback in this 2 minute survey about how we can make the process of accepting payments more useful for your store.', 'woocommerce-payments' ) ); $note->set_content_data( (object) [] ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-payments' ); $note->add_action( self::NOTE_NAME, __( 'Share feedback', 'woocommerce-payments' ), self::NOTE_DOCUMENTATION_URL, 'unactioned', true ); return $note; } } includes/notes/class-wc-payments-notes-set-https-for-checkout.php 0000777 00000003312 15251156605 0021247 0 ustar 00 <?php /** * Set up ensure https on checkout note for WooCommerce inbox. * * @package WooCommerce\Payments\Admin */ use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; defined( 'ABSPATH' ) || exit; /** * Class WC_Payments_Notes_Set_Https_For_Checkout */ class WC_Payments_Notes_Set_Https_For_Checkout { use NoteTraits { can_be_added as protected trait_can_be_added; } /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-payments-notes-set-https-for-checkout'; /** * Name of the note for use in the database. */ const NOTE_DOCUMENTATION_URL = 'https://woocommerce.com/document/ssl-and-https/#woocommerce-force-ssl-setting'; /** * Checks if a note can and should be added. * * @return bool */ public static function can_be_added() { // This note only makes sense if HTTPS is not enforced yet. if ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || wc_site_is_https() ) { return false; } return self::trait_can_be_added(); } /** * Get the note. */ public static function get_note() { $note = new Note(); $note->set_title( __( 'Enable secure checkout', 'woocommerce-payments' ) ); $note->set_content( __( 'Enable HTTPS on your checkout pages to display all available payment methods and protect your customers data.', 'woocommerce-payments' ) ); $note->set_content_data( (object) [] ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-payments' ); $note->add_action( self::NOTE_NAME, __( 'Read more', 'woocommerce-payments' ), self::NOTE_DOCUMENTATION_URL, 'unactioned', true ); return $note; } } includes/notes/class-wc-payments-notes-loan-approved.php 0000777 00000011712 15251156605 0017477 0 ustar 00 <?php /** * Display a notice to merchants that have WCPay installed to inform them * that a loan offer has been approved from Stripe. * * @package WooCommerce\Payments\Admin */ use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Admin\Notes\NoteTraits; defined( 'ABSPATH' ) || exit; /** * Class WC_Payments_Notes_Loan_Approved */ class WC_Payments_Notes_Loan_Approved { use NoteTraits; /** * Prefix of the note for use in the database. */ const NOTE_NAME = 'wc-payments-notes-loan-approved'; /** * Nonce action name */ const NOTE_ACTION = 'view-capital-page'; /** * Loan information to build the message. * * @var array */ private static $loan_info; /** * Get the note. */ public static function get_note() { $note = new Note(); $dummy_order = wc_create_order(); $dummy_order->set_currency( self::$loan_info['details']['currency'] ); $note->set_title( __( 'Your capital loan has been approved!', 'woocommerce-payments' ) ); $note->set_content( sprintf( /* Translators: %1: total amount lent to the merchant formatted in the account currency, %2: WooPayments */ __( 'Congratulations! Your capital loan has been approved and %1$s was deposited into the bank account linked to %2$s. You\'ll automatically repay the loan, plus a flat fee, through a fixed percentage of each %2$s transaction.', 'woocommerce-payments' ), WC_Payments_Explicit_Price_Formatter::get_explicit_price( wc_price( WC_Payments_Utils::interpret_stripe_amount( self::$loan_info['details']['advance_amount'] ), [ 'currency' => self::$loan_info['details']['currency'] ] ), $dummy_order ), 'WooPayments' ) ); $note->set_content_data( (object) [ 'advance_amount' => self::$loan_info['details']['advance_amount'], 'advance_paid_out_at' => self::$loan_info['details']['advance_paid_out_at'], ] ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-payments' ); $note->add_action( self::NOTE_NAME, __( 'View loan details', 'woocommerce-payments' ), admin_url( 'admin.php?page=wc-admin&path=/payments/loans' ), Note::E_WC_ADMIN_NOTE_UNACTIONED, true ); return $note; } /** * Add the note if it passes predefined conditions. */ public static function possibly_add_note() { // If we have the correct information, proceed. Otherwise, delete existing notes. if ( ! self::validate_inputs() ) { // We don't have the necessary info to create a note, do nothing. return; } // Check if the current loan info matches with the received one. If it matches, don't add a new one. if ( ! self::check_attached_loan_data_is_different() ) { // Loan paid out dates are the same, do nothing. return; } // Do the overridden work. if ( ! self::can_be_added() ) { return; } $new_note = self::get_note(); $new_note->save(); } /** * Check if the stored loan info has all the values we need. * * @return bool */ private static function validate_inputs() { // If the loan amount isn't set correctly, don't push the note, and delete the old one if exists. if ( ! isset( self::$loan_info['details']['currency'], self::$loan_info['details']['advance_amount'], self::$loan_info['details']['advance_paid_out_at'] ) || ! is_numeric( self::$loan_info['details']['advance_amount'] ) || empty( self::$loan_info['details']['currency'] ) ) { // There's something wrong with the loan information, delete the existing note, just in case of wrong information. return false; } self::$loan_info['details']['currency'] = strtoupper( self::$loan_info['details']['currency'] ); return true; } /** * Checks the saved paid out date on the previous note and deletes it if it doesn't match, to create a new one. * * @return bool */ private static function check_attached_loan_data_is_different() { $data_store = WC_Data_Store::load( 'admin-note' ); $note_ids = $data_store->get_notes_with_name( self::NOTE_NAME ); if ( ! empty( $note_ids ) ) { $note = Notes::get_note( $note_ids[0] ); if ( $note instanceof Note ) { $content_data = (array) $note->get_content_data(); if ( isset( $content_data['advance_paid_out_at'], $content_data['advance_amount'] ) ) { if ( self::$loan_info['details']['advance_paid_out_at'] === $content_data['advance_paid_out_at'] && self::$loan_info['details']['advance_amount'] === $content_data['advance_amount'] ) { // Note already exists for the current loan. No action will be taken. return false; } } // The note isn't for the current loan. Delete it to create a new one. $data_store->delete( $note ); } } return true; } /** * Sets the loan information on the class. * * @param array $loan_info loan information. */ public static function set_loan_details( array $loan_info ) { self::$loan_info = $loan_info; } } includes/notes/class-wc-payments-notes-set-up-stripelink.php 0000777 00000005355 15251156605 0020335 0 ustar 00 <?php /** * Set up Stripe Link note for WooCommerce inbox. * * @package WooCommerce\Payments\Admin */ use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\Notes\NoteTraits; use WCPay\PaymentMethods\Configs\Definitions\LinkDefinition; use WCPay\Payment_Methods\CC_Payment_Method; defined( 'ABSPATH' ) || exit; /** * Class WC_Payments_Notes_Setup_StripeLink */ class WC_Payments_Notes_Set_Up_StripeLink { use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-payments-notes-set-up-stripe-link'; /** * CTA button link */ const NOTE_DOCUMENTATION_URL = 'https://woocommerce.com/document/woopayments/payment-methods/link-by-stripe/'; /** * The account service instance. * * @var WC_Payment_Gateway_WCPay */ private static $gateway; /** * Checks if a note can and should be added. * * @return bool */ public static function should_display_note(): bool { // Check if Link payment is available. $available_upe_payment_methods = self::$gateway->get_upe_available_payment_methods(); if ( ! in_array( LinkDefinition::get_id(), $available_upe_payment_methods, true ) ) { return false; } // Retrieve enabled payment methods at checkout. $enabled_payment_methods = self::$gateway->get_payment_method_ids_enabled_at_checkout_filtered_by_fees( null, true ); // If card payment method is not enabled or Link payment method is enabled, skip. if ( ! in_array( CC_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $enabled_payment_methods, true ) || in_array( LinkDefinition::get_id(), $enabled_payment_methods, true ) ) { return false; } return true; } /** * Get the note. */ public static function get_note() { if ( ! self::should_display_note() ) { return; } $note = new Note(); $note->set_title( __( 'Increase conversion at checkout', 'woocommerce-payments' ) ); $note->set_content( __( 'Reduce cart abandonment and create a frictionless checkout experience with Link by Stripe. Link autofills your customer’s payment and shipping details, so they can check out in just six seconds with the Link optimized experience.', 'woocommerce-payments' ) ); $note->set_content_data( (object) [] ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_source( 'woocommerce-payments' ); $note->add_action( self::NOTE_NAME, __( 'Set up now', 'woocommerce-payments' ), self::NOTE_DOCUMENTATION_URL, 'unactioned', true ); return $note; } /** * Sets the WCPay gateway instance reference on the class. * * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway instance. */ public static function set_gateway( WC_Payment_Gateway_WCPay $gateway ) { self::$gateway = $gateway; } } includes/notes/class-wc-payments-notes-additional-payment-methods.php 0000777 00000001312 15251156605 0022147 0 ustar 00 <?php /** * Display a notice to merchants that have WCPay installed (and don't have UPE enabled) * to inform them that they can enable additional payment methods. * * @package WooCommerce\Payments\Admin */ use Automattic\WooCommerce\Admin\Notes\NoteTraits; defined( 'ABSPATH' ) || exit; /** * Class WC_Payments_Notes_Additional_Payment_Methods */ class WC_Payments_Notes_Additional_Payment_Methods { use NoteTraits; /** * Name of the note for use in the database. */ const NOTE_NAME = 'wc-payments-notes-additional-payment-methods'; /** * Get the note. */ public static function get_note() { // The notice should not be shown anymore because UPE is the default now. return false; } } includes/class-wc-payments-customer-service.php 0000777 00000044470 15251156605 0015760 0 ustar 00 <?php /** * Class WC_Payments_Customer * * @package WooCommerce\Payments */ use WCPay\Exceptions\API_Exception; use WCPay\Logger; use WCPay\Constants\Payment_Method; defined( 'ABSPATH' ) || exit; /** * Class handling any customer functionality */ class WC_Payments_Customer_Service { /** * Deprecated Stripe customer ID option. * * This option was used to store the customer_id in a WC_User options before we decoupled live and test customers. */ const DEPRECATED_WCPAY_CUSTOMER_ID_OPTION = '_wcpay_customer_id'; /** * Live Stripe customer ID option. * * This option is used to store new live mode customers in a WC_User options. Customers stored in the deprecated * option are migrated to this one. */ const WCPAY_LIVE_CUSTOMER_ID_OPTION = '_wcpay_customer_id_live'; /** * Test Stripe customer ID option. * * This option is used to store new test mode customer IDs in a WC_User options. */ const WCPAY_TEST_CUSTOMER_ID_OPTION = '_wcpay_customer_id_test'; /** * Key used to store customer id for non logged in users in WooCommerce Session. */ const CUSTOMER_ID_SESSION_KEY = 'wcpay_customer_id'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * WC_Payments_Account instance to get information about the account * * @var WC_Payments_Account */ private $account; /** * WC_Payments_Session_Service instance for working with session information * * @var WC_Payments_Session_Service */ private $session_service; /** * WC_Payments_Order_Service instance * * @var WC_Payments_Order_Service */ private $order_service; /** * Class constructor * * @param WC_Payments_API_Client $payments_api_client Payments API client. * @param WC_Payments_Account $account WC_Payments_Account instance. * @param WC_Payments_Session_Service $session_service Session Service class instance. * @param WC_Payments_Order_Service $order_service Order Service class instance. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account, WC_Payments_Session_Service $session_service, WC_Payments_Order_Service $order_service ) { $this->payments_api_client = $payments_api_client; $this->account = $account; $this->session_service = $session_service; $this->order_service = $order_service; } /** * Initialize hooks */ public function init_hooks() { /* * Adds the WooCommerce Payments customer ID found in the user session * to the WordPress user as metadata. * * This is helpful in scenarios where the shopper begins the checkout flow * logged out (i.e. guest checkout) and a user account is created for them * during checkout. * * This occurs when a user account is necessary for checkout, e.g. when the shopper * purchases a subscription product. */ add_action( 'woocommerce_created_customer', [ $this, 'add_customer_id_to_user' ] ); } /** * Get WCPay customer ID for the given WordPress user ID * * @param int|null $user_id The user ID to look for a customer ID with. * * @return string|null WCPay customer ID or null if not found. */ public function get_customer_id_by_user_id( $user_id ) { // User ID might be 0 if fetched from a WP_User instance for a user who isn't logged in. if ( null === $user_id || 0 === $user_id ) { // Try to retrieve the customer id from the session if stored previously. $customer_id = WC()->session ? WC()->session->get( self::CUSTOMER_ID_SESSION_KEY ) : null; return is_string( $customer_id ) ? $customer_id : null; } $customer_id = get_user_option( $this->get_customer_id_option(), $user_id ); // If customer_id is false it could mean that it hasn't been migrated from the deprecated key. if ( false === $customer_id ) { $this->maybe_migrate_deprecated_customer( $user_id ); // Customer might've been migrated in maybe_migrate_deprecated_customer, so we need to fetch it again. $customer_id = get_user_option( $this->get_customer_id_option(), $user_id ); } return $customer_id ? $customer_id : null; } /** * Create a customer and associate it with a WordPress user. * * @param WP_User|null $user User to create a customer for. * @param array $customer_data Customer data. * * @return string The created customer's ID * * @throws API_Exception Error creating customer. */ public function create_customer_for_user( ?WP_User $user, array $customer_data = [] ): string { // Include the session ID for the user. $customer_data['session_id'] = $this->session_service->get_sift_session_id() ?? null; // Create a customer on the WCPay server. $customer_id = $this->payments_api_client->create_customer( $customer_data ); if ( $user instanceof WP_User && $user->ID > 0 ) { $this->update_user_customer_id( $user->ID, $customer_id ); } if ( isset( WC()->session ) ) { // Save the customer id in the session for non logged in users to reuse it in payments. WC()->session->set( self::CUSTOMER_ID_SESSION_KEY, $customer_id ); } return $customer_id; } /** * Manages customer details held on WCPay server for WordPress user associated with an order. * * @param int|null $user_id ID of the WP user to associate with the customer. * @param WC_Order $order Woo Order. * * @return string WooPayments customer ID. * @throws API_Exception Throws when server API request fails. */ public function get_or_create_customer_id_from_order( ?int $user_id, WC_Order $order ): string { // Determine the customer making the payment, create one if we don't have one already. $customer_id = $this->get_customer_id_by_user_id( $user_id ); $customer_data = self::map_customer_data( $order, new WC_Customer( $user_id ?? 0 ) ); $user = null === $user_id ? null : get_user_by( 'id', $user_id ); if ( null !== $customer_id ) { $this->update_customer_for_user( $customer_id, $user, $customer_data ); return $customer_id; } return $this->create_customer_for_user( $user, $customer_data ); } /** * Update the customer details held on the WCPay server associated with the given WordPress user. * * @param string $customer_id WCPay customer ID. * @param WP_User|null $user WordPress user. * @param array $customer_data Customer data. * * @return string The updated customer's ID. Can be different to the ID parameter if the customer was re-created. * * @throws API_Exception Error updating the customer. */ public function update_customer_for_user( string $customer_id, ?WP_User $user, array $customer_data ): string { try { // Update the customer on the WCPay server. $this->payments_api_client->update_customer( $customer_id, $customer_data ); // We successfully updated the existing customer, so return the passed in ID unchanged. return $customer_id; } catch ( API_Exception $e ) { // If we failed to find the customer we wanted to update, then create a new customer and associate it to the // current user instead. This might happen if the customer was deleted from the server, the linked WCPay // account was changed, or if users were imported from another site. if ( $e->get_error_code() === 'resource_missing' ) { // Create a new customer to associate with this user. We'll return the new customer ID. return $this->recreate_customer( $user, $customer_data ); } // For any other type of exception, just re-throw. throw $e; } } /** * Sets a payment method as default for a customer. * * @param string $customer_id The customer ID. * @param string $payment_method_id The payment method ID. */ public function set_default_payment_method_for_customer( $customer_id, $payment_method_id ) { $this->payments_api_client->update_customer( $customer_id, [ 'invoice_settings' => [ 'default_payment_method' => $payment_method_id, ], ] ); } /** * Gets all payment methods for a customer. * * @param string $customer_id The customer ID. * @param string $type Type of payment methods to fetch. * @return array * * @throws API_Exception We only handle 'resource_missing' code types and rethrow anything else. */ public function get_payment_methods_for_customer( $customer_id, $type = 'card' ) { if ( ! $customer_id ) { return []; } try { return $this->payments_api_client->get_payment_methods( $customer_id, $type )['data']; } catch ( API_Exception $e ) { // If we failed to find the payment methods, we can simply return empty payment methods as this customer // will be recreated when the user successfully adds a payment method. if ( $e->get_error_code() === 'resource_missing' ) { return []; } // Rethrow for error codes we don't care about in this function. throw $e; } } /** * Updates a customer payment method. * * @param string $payment_method_id The payment method ID. * @param WC_Order $order Order to be used on the update. */ public function update_payment_method_with_billing_details_from_order( $payment_method_id, $order ) { $billing_details = $this->order_service->get_billing_data_from_order( $order ); if ( ! empty( $billing_details ) ) { $this->payments_api_client->update_payment_method( $payment_method_id, [ 'billing_details' => $billing_details, ] ); } } /** * Given a WC_Order or WC_Customer, returns an array representing a Stripe customer object. * At least one parameter has to not be null. * * @param WC_Order $wc_order The Woo order to parse. * @param WC_Customer $wc_customer The Woo customer to parse. * * @return array Customer data. */ public static function map_customer_data( ?WC_Order $wc_order = null, ?WC_Customer $wc_customer = null ): array { if ( null === $wc_customer && null === $wc_order ) { return []; } // Where available, the order data takes precedence over the customer. $object_to_parse = $wc_order ?? $wc_customer; $name = $object_to_parse->get_billing_first_name() . ' ' . $object_to_parse->get_billing_last_name(); $description = ''; if ( null !== $wc_customer && ! empty( $wc_customer->get_username() ) ) { // We have a logged in user, so add their username to the customer description. // translators: %1$s Name, %2$s Username. $description = sprintf( __( 'Name: %1$s, Username: %2$s', 'woocommerce-payments' ), $name, $wc_customer->get_username() ); } else { // Current user is not logged in. // translators: %1$s Name. $description = sprintf( __( 'Name: %1$s, Guest', 'woocommerce-payments' ), $name ); } $data = [ 'name' => $name, 'description' => $description, 'email' => $object_to_parse->get_billing_email(), 'phone' => $object_to_parse->get_billing_phone(), 'address' => [ 'line1' => $object_to_parse->get_billing_address_1(), 'line2' => $object_to_parse->get_billing_address_2(), 'postal_code' => $object_to_parse->get_billing_postcode(), 'city' => $object_to_parse->get_billing_city(), 'state' => $object_to_parse->get_billing_state(), 'country' => $object_to_parse->get_billing_country(), ], ]; if ( ! empty( $object_to_parse->get_shipping_postcode() ) ) { $data['shipping'] = [ 'name' => $object_to_parse->get_shipping_first_name() . ' ' . $object_to_parse->get_shipping_last_name(), 'address' => [ 'line1' => $object_to_parse->get_shipping_address_1(), 'line2' => $object_to_parse->get_shipping_address_2(), 'postal_code' => $object_to_parse->get_shipping_postcode(), 'city' => $object_to_parse->get_shipping_city(), 'state' => $object_to_parse->get_shipping_state(), 'country' => $object_to_parse->get_shipping_country(), ], ]; } return $data; } /** * Recreates the customer for this user. * * @param WP_User|null $user User to recreate a customer for. * @param array $customer_data Customer data. * * @return string The newly created customer's ID * * @throws API_Exception Error creating customer. */ private function recreate_customer( ?WP_User $user, array $customer_data ): string { if ( $user instanceof WP_User && $user->ID > 0 ) { $result = delete_user_option( $user->ID, $this->get_customer_id_option() ); if ( ! $result ) { // Log the error, but continue since we'll be trying to update this option in create_customer. Logger::error( 'Failed to delete old customer ID for user ' . $user->ID ); } } return $this->create_customer_for_user( $user, $customer_data ); } /** * Returns the name of the customer option meta, taking test mode into account. * * @return string The customer ID option name. */ private function get_customer_id_option(): string { return WC_Payments::mode()->is_test() ? self::WCPAY_TEST_CUSTOMER_ID_OPTION : self::WCPAY_LIVE_CUSTOMER_ID_OPTION; } /** * Migrate any customer ID that might be in the DEPRECATED_WCPAY_CUSTOMER_ID_OPTION. * * @param int $user_id The user ID to look for a customer ID with. */ private function maybe_migrate_deprecated_customer( $user_id ) { $customer_id = get_user_option( self::DEPRECATED_WCPAY_CUSTOMER_ID_OPTION, $user_id ); if ( false !== $customer_id ) { // A customer was found in the deprecated key. Migrate it to the appropriate one and delete the old meta. // If an account is live mode, we optimistically assume that the customer is live mode, to avoid losing // live mode customer data. If the account is not live mode, it can only have test mode objects, so we // can safely migrate them to the test key. // If is_live cannot be determined, default it to true to avoid considering a live account as test. $account_is_live = null === $this->account->get_is_live() || $this->account->get_is_live(); $customer_option_id = $account_is_live ? self::WCPAY_LIVE_CUSTOMER_ID_OPTION : self::WCPAY_TEST_CUSTOMER_ID_OPTION; if ( update_user_option( $user_id, $customer_option_id, $customer_id ) ) { delete_user_option( $user_id, self::DEPRECATED_WCPAY_CUSTOMER_ID_OPTION ); } else { Logger::error( 'Failed to store new customer ID for user ' . $user_id . '; legacy customer was kept.' ); } } } /** * Get the WCPay customer ID associated with an order, or create one if none found. * * @param WC_Order $order WC Order object. * * @return string|null WCPay customer ID. * @throws API_Exception If there's an error creating customer. */ public function get_customer_id_for_order( $order ) { $customer_id = null; $user = $order->get_user(); if ( false !== $user ) { // Determine the customer making the payment, create one if we don't have one already. $customer_id = $this->get_customer_id_by_user_id( $user->ID ); if ( null === $customer_id ) { $customer_data = self::map_customer_data( $order, new WC_Customer( $user->ID ) ); $customer_id = $this->create_customer_for_user( $user, $customer_data ); } } return $customer_id; } /** * Updates the given user with the given WooCommerce Payments * customer ID. * * @param int $user_id The WordPress user ID. * @param string $customer_id The WooCommerce Payments customer ID. */ public function update_user_customer_id( int $user_id, string $customer_id ) { $global = WC_Payments::is_network_saved_cards_enabled(); $result = update_user_option( $user_id, $this->get_customer_id_option(), $customer_id, $global ); if ( ! $result ) { Logger::error( 'Failed to update customer ID for user ' . $user_id ); } } /** * Adds the WooCommerce Payments customer ID found in the user session * to the WordPress user as metadata. * * @param int $user_id The WordPress user ID. */ public function add_customer_id_to_user( $user_id ) { // Not processing a checkout, bail. if ( ! ( defined( 'WOOCOMMERCE_CHECKOUT' ) && WOOCOMMERCE_CHECKOUT ) && ! ( function_exists( 'wcs_is_checkout_blocks_api_request' ) && wcs_is_checkout_blocks_api_request( 'v1/checkout' ) ) ) { return; } // Retrieve the WooCommerce Payments customer ID from the user session. $customer_id = WC()->session ? WC()->session->get( self::CUSTOMER_ID_SESSION_KEY ) : null; if ( ! $customer_id ) { return; } $this->update_user_customer_id( $user_id, $customer_id ); } /** * Prepares customer data to be used on 'Pay for Order' or 'Add Payment Method' pages. * Customer data is retrieved from order when on Pay for Order. * Customer data is retrieved from customer when on 'Add Payment Method'. * * @return array|null An array with customer data or nothing. */ public function get_prepared_customer_data() { if ( ! isset( $_GET['pay_for_order'] ) && ! is_add_payment_method_page() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return null; } global $wp; $user_email = ''; $firstname = ''; $lastname = ''; $billing_country = ''; $address = null; if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $order_id = absint( $wp->query_vars['order-pay'] ); $order = wc_get_order( $order_id ); if ( is_a( $order, 'WC_Order' ) ) { $firstname = $order->get_billing_first_name(); $lastname = $order->get_billing_last_name(); $user_email = $order->get_billing_email(); $billing_country = $order->get_billing_country(); $address = [ 'city' => $order->get_billing_city(), 'country' => $order->get_billing_country(), 'line1' => $order->get_billing_address_1(), 'line2' => $order->get_billing_address_2(), 'postal_code' => $order->get_billing_postcode(), 'state' => $order->get_billing_state(), ]; } } if ( is_add_payment_method_page() ) { $user = wp_get_current_user(); if ( $user->ID ) { $firstname = $user->user_firstname; $lastname = $user->user_lastname; $user_email = get_user_meta( $user->ID, 'billing_email', true ); $user_email = ! empty( $user_email ) ? $user_email : $user->user_email; $billing_country = get_user_meta( $user->ID, 'billing_country', true ); } } return [ 'name' => $firstname . ' ' . $lastname, 'email' => $user_email, 'billing_country' => $billing_country, 'address' => $address, ]; } } includes/class-wc-payments-captured-event-note.php 0000777 00000047356 15251156605 0016360 0 ustar 00 <?php /** * Class WC_Payments_Captured_Event_Note * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Utility class generating detailed captured note for successful payments. */ class WC_Payments_Captured_Event_Note { const HTML_BLACK_BULLET = '<span style="font-size: 7px;vertical-align: middle;">●</span>'; const HTML_WHITE_BULLET = '<span style="font-size: 7px;vertical-align: middle;">○</span>'; const HTML_SPACE = ' '; const HTML_BR = '<br>'; /** * Captured event data. * * @var array */ private $captured_event; /** * Constructor. * * @param array $captured_event Captured event data. * * @throws Exception */ public function __construct( array $captured_event ) { $is_captured_event = isset( $captured_event['type'] ) && 'captured' === $captured_event['type']; if ( ! $is_captured_event ) { throw new Exception( 'Not a captured event' ); } $this->captured_event = $captured_event; } /** * Generate the HTML note. * * @return string */ public function generate_html_note(): string { $lines = []; $fx_string = $this->compose_fx_string(); if ( null !== $fx_string ) { $lines[] = $fx_string; } $lines[] = $this->compose_fee_string(); $fee_breakdown_lines = $this->compose_fee_break_down(); if ( null !== $fee_breakdown_lines ) { $lines = array_merge( $lines, $fee_breakdown_lines ); } if ( $this->has_tax() ) { $lines[] = $this->compose_tax_string(); } $lines[] = $this->compose_net_string(); $html = ''; foreach ( $lines as $line ) { $html .= '<p>' . $line . '</p>' . PHP_EOL; } return '<div class="captured-event-details">' . PHP_EOL . $html . '</div>'; } /** * Generate FX string. * * @return string|null */ public function compose_fx_string() { if ( ! $this->is_fx_event() ) { return null; } $customer_currency = $this->captured_event['transaction_details']['customer_currency']; $customer_amount_captured = $this->captured_event['transaction_details']['customer_amount_captured']; $store_currency = $this->captured_event['transaction_details']['store_currency']; $store_amount_captured = $this->captured_event['transaction_details']['store_amount_captured']; return $this->format_fx( $customer_currency, $customer_amount_captured, $store_currency, $store_amount_captured ); } /** * Generate fee string. * * @return string */ public function compose_fee_string(): string { $data = $this->captured_event; $fee_rates = $data['fee_rates']; $percentage = $fee_rates['percentage']; $fixed_currency = $fee_rates['fixed_currency']; $fixed = WC_Payments_Utils::interpret_stripe_amount( (int) $fee_rates['fixed'], $fixed_currency ); $history = $fee_rates['history']; if ( $this->has_tax() ) { $before_tax = $data['fee_rates']['before_tax']; $fee_amount = $before_tax['amount']; $fee_currency = $before_tax['currency']; } else { $fee_currency = $data['transaction_details']['customer_currency']; $fee_amount = (int) $data['transaction_details']['customer_fee']; } $formatted_fee_amount = $this->convert_and_format_fee_amount( $fee_amount, $fee_currency ); $base_fee_label = $this->is_base_fee_only() ? __( 'Base fee', 'woocommerce-payments' ) : __( 'Fee', 'woocommerce-payments' ); $is_capped = isset( $history[0]['capped'] ) && true === $history[0]['capped']; if ( $this->is_base_fee_only() && $is_capped ) { return sprintf( '%1$s (capped at %2$s): %3$s', $base_fee_label, WC_Payments_Utils::format_currency( $fixed, $fixed_currency ), $formatted_fee_amount ); } $is_same_symbol = $this->has_same_currency_symbol( $data['transaction_details']['store_currency'], $data['transaction_details']['customer_currency'] ); return sprintf( '%1$s (%2$s%% + %3$s%4$s): %5$s%6$s', $base_fee_label, self::format_fee( $percentage ), WC_Payments_Utils::format_currency( $fixed, $fixed_currency ), $is_same_symbol ? ' ' . $data['transaction_details']['customer_currency'] : '', $formatted_fee_amount, $is_same_symbol ? " {$data['transaction_details']['store_currency']}" : '' ); } /** * Generate an array including HTML formatted breakdown lines. * * @return array<string>|null */ public function compose_fee_break_down() { $fee_history_strings = $this->get_fee_breakdown(); if ( null === $fee_history_strings ) { return null; } if ( 0 === count( $fee_history_strings ) ) { return null; } $res = []; foreach ( $fee_history_strings as $type => $fee ) { $res[] = self::HTML_BLACK_BULLET . ' ' . ( 'discount' === $type ? $fee['label'] : $fee ); if ( 'discount' === $type ) { $res[] = str_repeat( self::HTML_SPACE . ' ', 2 ) . self::HTML_WHITE_BULLET . ' ' . $fee['variable']; $res[] = str_repeat( self::HTML_SPACE . ' ', 2 ) . self::HTML_WHITE_BULLET . ' ' . $fee['fixed']; } } return $res; } /** * Generate net string. * * @return string */ public function compose_net_string(): string { $data = $this->captured_event['transaction_details']; // Determine the type of payment and select the appropriate amounts and currencies. if ( $this->is_fx_event() ) { // For fx events, we need the store amount and currency to display the net amount // in the store currency. $amount = $data['store_amount']; $captured_amount = $data['store_amount_captured']; $fee = $data['store_fee']; $currency = $data['store_currency']; } else { $amount = $data['customer_amount']; $captured_amount = $data['customer_amount_captured']; $fee = $data['customer_fee']; $currency = $data['customer_currency']; } $gross_amount = $captured_amount ?? $amount; $net = WC_Payments_Utils::interpret_stripe_amount( (int) ( $gross_amount - $fee ), $currency ); // Format and return the net string. return sprintf( /* translators: %s is a monetary amount */ __( 'Net payout: %s', 'woocommerce-payments' ), WC_Payments_Utils::format_explicit_currency( $net, $currency ) ); } /** * Returns an associative array containing fee breakdown. * Keys are fee types such as base, additional-fx, etc, except for "discount" that is an associative array including more discount details. * * @return array|null */ public function get_fee_breakdown() { $data = $this->captured_event; if ( ! isset( $data['fee_rates']['history'] ) ) { return null; } $history = $data['fee_rates']['history']; // Hide breakdown when there's only a base fee. if ( $this->is_base_fee_only() ) { return null; } $fee_history_strings = []; foreach ( $history as $fee ) { $label_type = $fee['type']; if ( $fee['additional_type'] ?? '' ) { $label_type .= '-' . $fee['additional_type']; } $percentage_rate = (float) $fee['percentage_rate']; $fixed_rate = (int) $fee['fixed_rate']; $currency = strtoupper( $fee['currency'] ); $is_capped = isset( $fee['capped'] ) && true === $fee['capped']; $percentage_rate_formatted = self::format_fee( $percentage_rate ); $fix_rate_formatted = WC_Payments_Utils::format_currency( WC_Payments_Utils::interpret_stripe_amount( $fixed_rate ), $currency ); if ( $this->has_same_currency_symbol( $data['transaction_details']['customer_currency'], $data['transaction_details']['store_currency'] ) ) { $fix_rate_formatted = $fix_rate_formatted . ' ' . $data['transaction_details']['store_currency']; } $label = sprintf( $this->fee_label_mapping( $fixed_rate, $is_capped )[ $label_type ], $percentage_rate_formatted, $fix_rate_formatted ); if ( 'discount' === $label_type ) { $fee_history_strings[ $label_type ] = [ 'label' => $label, 'variable' => sprintf( /* translators: %s is a percentage number */ __( 'Variable fee: %s', 'woocommerce-payments' ), $percentage_rate_formatted ) . '%', 'fixed' => sprintf( /* translators: %s is a monetary amount */ __( 'Fixed fee: %s', 'woocommerce-payments' ), $fix_rate_formatted ), ]; } else { $fee_history_strings[ $label_type ] = $label; } } return $fee_history_strings; } /** * Compose tax string. * * @return string|null */ public function compose_tax_string(): ?string { if ( ! $this->has_tax() ) { return null; } $tax = $this->captured_event['fee_rates']['tax']; $tax_amount = $tax['amount']; if ( 0 === $tax_amount ) { return null; } $tax_currency = $tax['currency']; $formatted_amount = $this->convert_and_format_fee_amount( $tax_amount, $tax_currency ); $tax_description = ' ' . $this->get_localized_tax_description(); $percentage_rate = $tax['percentage_rate']; $formatted_percentage = ' (' . self::format_fee( $percentage_rate ) . '%)'; return sprintf( /* translators: 1: tax description 2: tax percentage 3: tax amount */ __( 'Tax%1$s%2$s: %3$s', 'woocommerce-payments' ), $tax_description, $formatted_percentage, $formatted_amount ); } /** * Check if this is a FX event. * * @return bool */ private function is_fx_event(): bool { $customer_currency = $this->captured_event['transaction_details']['customer_currency'] ?? null; $store_currency = $this->captured_event['transaction_details']['store_currency'] ?? null; return ! ( is_null( $customer_currency ) || is_null( $store_currency ) || $customer_currency === $store_currency ); } /** * Return a boolean indicating whether only fee applied is the base fee. * * @return bool True if the only applied fee is the base fee */ private function is_base_fee_only(): bool { if ( ! isset( $this->captured_event['fee_rates']['history'] ) ) { return false; } $history = $this->captured_event['fee_rates']['history']; return 1 === ( is_countable( $history ) ? count( $history ) : 0 ) && 'base' === $history[0]['type']; } /** * Get the mapping format for all types of fees. * * @param int $fixed_rate Fixed rate amount in Stripe format. * @param bool $is_capped True if the fee is capped. * * @return array An associative array with keys are fee types, values are string formats. */ private function fee_label_mapping( int $fixed_rate, bool $is_capped ) { $res = []; $res['base'] = $is_capped /* translators: %2$s is the capped fee */ ? __( 'Base fee: capped at %2$s', 'woocommerce-payments' ) : ( 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ ? __( 'Base fee: %1$s%% + %2$s', 'woocommerce-payments' ) /* translators: %1$s% is the fee percentage */ : __( 'Base fee: %1$s%%', 'woocommerce-payments' ) ); $res['additional-international'] = 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ ? __( 'International card fee: %1$s%% + %2$s', 'woocommerce-payments' ) /* translators: %1$s% is the fee percentage */ : __( 'International card fee: %1$s%%', 'woocommerce-payments' ); $res['additional-fx'] = 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ ? __( 'Currency conversion fee: %1$s%% + %2$s', 'woocommerce-payments' ) /* translators: %1$s% is the fee percentage */ : __( 'Currency conversion fee: %1$s%%', 'woocommerce-payments' ); $res['additional-wcpay-subscription'] = 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ ? __( 'Subscription transaction fee: %1$s%% + %2$s', 'woocommerce-payments' ) /* translators: %1$s% is the fee percentage */ : __( 'Subscription transaction fee: %1$s%%', 'woocommerce-payments' ); $res['discount'] = __( 'Discount', 'woocommerce-payments' ); return $res; } /** * Return a given decimal fee as a percentage with a maximum of 3 decimal places. * * @param float $percentage Percentage as float. * * @return string */ private function format_fee( float $percentage ): string { return (string) round( $percentage * 100, 3 ); } /** * Format FX string based on the two provided currencies. * * @param string $from_currency 3-letter code for original currency. * @param int $from_amount Amount (Stripe-type) for original currency. * @param string $to_currency 3-letter code for converted currency. * @param int $to_amount Amount (Stripe-type) for converted currency. * * @return string Formatted FX string. */ private function format_fx( string $from_currency, int $from_amount, string $to_currency, int $to_amount ): string { $exchange_rate = (float) ( 0 !== $from_amount ? $to_amount / $from_amount : 0 ); if ( WC_Payments_Utils::is_zero_decimal_currency( strtolower( $to_currency ) ) ) { $exchange_rate *= 100; } if ( WC_Payments_Utils::is_zero_decimal_currency( strtolower( $from_currency ) ) ) { $exchange_rate /= 100; } $to_display_amount = WC_Payments_Utils::interpret_stripe_amount( $to_amount, $to_currency ); return sprintf( '%1$s → %2$s: %3$s', self::format_explicit_currency_with_base( 1, $from_currency, $to_currency, true ), self::format_exchange_rate( $exchange_rate, $to_currency ), WC_Payments_Utils::format_explicit_currency( $to_display_amount, $to_currency, false ) ); } /** * Format exchange rate. * * @param float $rate Exchange rate. * @param string $currency 3-letter currency code. * * @return string */ private function format_exchange_rate( float $rate, string $currency ): string { $num_decimals = $rate > 1 ? 5 : 6; $formatted = WC_Payments_Utils::format_explicit_currency( $rate, $currency, true, [ 'decimals' => $num_decimals ] ); $func_remove_ending_zeros = function ( $str ) { return rtrim( $str, '0' ); }; // Remove ending zeroes after the decimal separator if they exist. return implode( ' ', array_map( $func_remove_ending_zeros, explode( ' ', $formatted ) ) ); } /** * Format amount for a given currency but according to the base currency's format. * * @param float $amount Amount. * @param string $currency 3-letter currency code. * @param string $base_currency 3-letter base currency code. * @param bool $skip_symbol Optional. If true, trims off the short currency symbol. Default false. * * @return string */ private function format_explicit_currency_with_base( float $amount, string $currency, string $base_currency, bool $skip_symbol = false ) { $custom_format = WC_Payments_Utils::get_currency_format_for_wc_price( $base_currency ); unset( $custom_format['currency'] ); // Given this is used to display the $amount, the decimals for $base_currency shouldn't interfere with decimals for $currency. $custom_format['decimals'] = WC_Payments_Utils::get_currency_format_for_wc_price( $currency )['decimals']; return WC_Payments_Utils::format_explicit_currency( $amount, $currency, $skip_symbol, $custom_format ); } /** * Compare does two currencies have the same symbol. * * @param string $base_currency Base currency. * @param string $currency Currency to compare. * * @return bool */ private function has_same_currency_symbol( string $base_currency, string $currency ): bool { return strcasecmp( $base_currency, $currency ) !== 0 && get_woocommerce_currency_symbol( $base_currency ) === get_woocommerce_currency_symbol( $currency ); } /** * Check if the event has tax information. * * @return bool */ private function has_tax(): bool { return isset( $this->captured_event['fee_rates']['tax'] ); } /** * Get localized tax description based on the tax description ID contained in the captured event. * * @return string|null */ private function get_localized_tax_description(): ?string { if ( ! isset( $this->captured_event['fee_rates']['tax']['description'] ) ) { return null; } $tax_description_id = $this->captured_event['fee_rates']['tax']['description']; $tax_descriptions = [ // European Union VAT. 'AT VAT' => __( 'AT VAT', 'woocommerce-payments' ), // Austria. 'BE VAT' => __( 'BE VAT', 'woocommerce-payments' ), // Belgium. 'BG VAT' => __( 'BG VAT', 'woocommerce-payments' ), // Bulgaria. 'CY VAT' => __( 'CY VAT', 'woocommerce-payments' ), // Cyprus. 'CZ VAT' => __( 'CZ VAT', 'woocommerce-payments' ), // Czech Republic. 'DE VAT' => __( 'DE VAT', 'woocommerce-payments' ), // Germany. 'DK VAT' => __( 'DK VAT', 'woocommerce-payments' ), // Denmark. 'EE VAT' => __( 'EE VAT', 'woocommerce-payments' ), // Estonia. 'ES VAT' => __( 'ES VAT', 'woocommerce-payments' ), // Spain. 'FI VAT' => __( 'FI VAT', 'woocommerce-payments' ), // Finland. 'FR VAT' => __( 'FR VAT', 'woocommerce-payments' ), // France. 'GB VAT' => __( 'UK VAT', 'woocommerce-payments' ), // United Kingdom. 'GR VAT' => __( 'GR VAT', 'woocommerce-payments' ), // Greece. 'HR VAT' => __( 'HR VAT', 'woocommerce-payments' ), // Croatia. 'HU VAT' => __( 'HU VAT', 'woocommerce-payments' ), // Hungary. 'IE VAT' => __( 'IE VAT', 'woocommerce-payments' ), // Ireland. 'IT VAT' => __( 'IT VAT', 'woocommerce-payments' ), // Italy. 'LT VAT' => __( 'LT VAT', 'woocommerce-payments' ), // Lithuania. 'LU VAT' => __( 'LU VAT', 'woocommerce-payments' ), // Luxembourg. 'LV VAT' => __( 'LV VAT', 'woocommerce-payments' ), // Latvia. 'MT VAT' => __( 'MT VAT', 'woocommerce-payments' ), // Malta. 'NO VAT' => __( 'NO VAT', 'woocommerce-payments' ), // Norway. 'NL VAT' => __( 'NL VAT', 'woocommerce-payments' ), // Netherlands. 'PL VAT' => __( 'PL VAT', 'woocommerce-payments' ), // Poland. 'PT VAT' => __( 'PT VAT', 'woocommerce-payments' ), // Portugal. 'RO VAT' => __( 'RO VAT', 'woocommerce-payments' ), // Romania. 'SE VAT' => __( 'SE VAT', 'woocommerce-payments' ), // Sweden. 'SI VAT' => __( 'SI VAT', 'woocommerce-payments' ), // Slovenia. 'SK VAT' => __( 'SK VAT', 'woocommerce-payments' ), // Slovakia. // GST Countries. 'AU GST' => __( 'AU GST', 'woocommerce-payments' ), // Australia. 'NZ GST' => __( 'NZ GST', 'woocommerce-payments' ), // New Zealand. 'SG GST' => __( 'SG GST', 'woocommerce-payments' ), // Singapore. // Other Tax Systems. 'CH VAT' => __( 'CH VAT', 'woocommerce-payments' ), // Switzerland. 'JP JCT' => __( 'JP JCT', 'woocommerce-payments' ), // Japan Consumption Tax. ]; return $tax_descriptions[ $tax_description_id ] ?? __( 'Tax', 'woocommerce-payments' ); } /** * Given the fee amount and currency, converts it to the store currency if necessary and formats using formatCurrency. * * @param float $fee_amount Fee amount to convert and format. * @param string $fee_currency Fee currency to convert from. * * @return string Formatted fee amount in the store currency. */ private function convert_and_format_fee_amount( float $fee_amount, string $fee_currency ) { $fee_exchange_rate = $this->captured_event['fee_rates']['fee_exchange_rate'] ?? null; $store_currency = $this->captured_event['transaction_details']['store_currency'] ?? null; if ( ( strtoupper( $fee_currency ) === strtoupper( $store_currency ) ) || ! $this->is_fx_event() || ! $fee_exchange_rate ) { return WC_Payments_Utils::format_currency( -abs( WC_Payments_Utils::interpret_stripe_amount( $fee_amount, $fee_currency ) ), $fee_currency ); } $rate = $fee_exchange_rate['rate']; $from_currency = $fee_exchange_rate['from_currency'] ?? null; // Convert based on the direction of the exchange rate. $converted_amount = strtoupper( $fee_currency ) === strtoupper( $from_currency ) ? $fee_amount / $rate // Converting from store currency to customer currency. : $fee_amount * $rate; // Converting from customer currency to store currency. return WC_Payments_Utils::format_currency( -abs( WC_Payments_Utils::interpret_stripe_amount( $converted_amount, $store_currency ) ), $store_currency ); } } includes/class-wc-payments-utils.php 0000777 00000150214 15251156605 0013613 0 ustar 00 <?php /** * Class WC_Payments_Utils * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Exceptions\{ Amount_Too_Small_Exception, API_Exception, Connection_Exception }; use WCPay\Constants\Country_Code; use WCPay\Constants\Currency_Code; /** * WC Payments Utils class */ class WC_Payments_Utils { /** * Max depth used when processing arrays, for example in redact_array. */ const MAX_ARRAY_DEPTH = 10; /** * Order meta data key that holds the currency of order's intent transaction. */ const ORDER_INTENT_CURRENCY_META_KEY = '_wcpay_intent_currency'; /** * Force disconnected flag name. */ const FORCE_DISCONNECTED_FLAG_NAME = 'wcpaydev_force_disconnected'; /** * The Store API route patterns that should be handled by the WooPay session handler. */ const STORE_API_ROUTE_PATTERNS = [ '@^\/wc\/store(\/v[\d]+)?\/cart$@', '@^\/wc\/store(\/v[\d]+)?\/cart\/add-item$@', '@^\/wc\/store(\/v[\d]+)?\/cart\/remove-item$@', '@^\/wc\/store(\/v[\d]+)?\/cart\/apply-coupon$@', '@^\/wc\/store(\/v[\d]+)?\/cart\/remove-coupon$@', '@^\/wc\/store(\/v[\d]+)?\/cart\/select-shipping-rate$@', '@^\/wc\/store(\/v[\d]+)?\/cart\/update-customer$@', '@^\/wc\/store(\/v[\d]+)?\/cart\/update-item$@', '@^\/wc\/store(\/v[\d]+)?\/cart\/extensions$@', '@^\/wc\/store(\/v[\d]+)?\/checkout\/(?P<id>[\d]+)@', '@^\/wc\/store(\/v[\d]+)?\/checkout$@', '@^\/wc\/store(\/v[\d]+)?\/order\/(?P<id>[\d]+)@', // The route below is not a Store API route. However, this REST endpoint is used by WooPay to indirectly reach the Store API. // By adding it to this list, we're able to identify the user and load the correct session for this route. '@^\/payments\/woopay\/session$@', ]; /** * Mirrors JS's createInterpolateElement functionality. * Returns a string where angle brackets expressions are replaced with unescaped html while the rest is escaped. * * @param string $text string to process. * @param array $element_map map of elements to not escape. * * @return string String where all of the html was escaped, except for the tags specified in element map. */ public static function esc_interpolated_html( $text, $element_map ) { // Regex to match string expressions wrapped in angle brackets. $tokenizer = '/<(\/)?(\w+)\s*(\/)?>/'; $string_queue = []; $token_queue = []; $last_mapped = true; // Start with a copy of the string. $processed = $text; // Match every angle bracket expression. while ( preg_match( $tokenizer, $processed, $matches ) ) { $matched = $matches[0]; $token = $matches[2]; // Determine if the expression is closing (</a>) or self-closed (<img />). $is_closing = ! empty( $matches[1] ); $is_self_closed = ! empty( $matches[3] ); // Split the string on the current matched token. $split = explode( $matched, $processed, 2 ); if ( $last_mapped ) { // If the previous token was present in the element map, or we're at the start, put the string between it and the current token in the queue. $string_queue[] = $split[0]; } else { // If the previous token was not found in the elements map, append it together with the string before the current token to the last item in the queue. $string_queue[ count( $string_queue ) - 1 ] .= $split[0]; } // String is now the bit after the current token. $processed = $split[1]; // Check if the current token is in the map. if ( isset( $element_map[ $token ] ) ) { $map_matched = preg_match( '/^<(\w+)(\s.+?)?\/?>$/', $element_map[ $token ], $map_matches ); if ( ! $map_matches ) { // Should not happen with the properly formatted html as map value. Return the whole string escaped. return esc_html( $text ); } // Add the matched token and its attributes into the token queue. It will not be escaped when constructing the final string. $tag = $map_matches[1]; $attrs = isset( $map_matches[2] ) ? $map_matches[2] : ''; if ( $is_closing ) { $token_queue[] = '</' . $tag . '>'; } elseif ( $is_self_closed ) { $token_queue[] = '<' . $tag . $attrs . '/>'; } else { $token_queue[] = '<' . $tag . $attrs . '>'; } // Mark the current token as found in the map. $last_mapped = true; } else { // Append the current token into the string queue. It will be escaped. $string_queue[ count( $string_queue ) - 1 ] .= $matched; // Mark the current token as not found in the map. $last_mapped = false; } } // No mapped tokens were found in the string, or token and string queues are not of equal length. // The latter should not happen - token queue and string queue should be the same length. if ( empty( $token_queue ) || count( $token_queue ) !== count( $string_queue ) ) { return esc_html( $text ); } // Construct the final string by escaping the string queue values and not escaping the token queue. $result = ''; while ( ! empty( $token_queue ) ) { $result .= esc_html( array_shift( $string_queue ) ) . array_shift( $token_queue ); } $result .= esc_html( $processed ); return $result; } /** * Returns an API-ready amount based on a currency. * * @param float $amount The base amount. * @param string $currency The currency for the amount. * * @return int The amount in cents. */ public static function prepare_amount( $amount, $currency = Currency_Code::UNITED_STATES_DOLLAR ): int { $conversion_rate = 100; if ( self::is_zero_decimal_currency( strtolower( $currency ) ) ) { $conversion_rate = 1; } return (int) round( (float) $amount * $conversion_rate ); } /** * Interprets amount from Stripe API. * * @param int $amount The amount returned by Stripe API. * @param string $currency The currency we get from Stripe API for the amount. * * @return float The interpreted amount. */ public static function interpret_stripe_amount( int $amount, string $currency = 'usd' ): float { $conversion_rate = 100; if ( self::is_zero_decimal_currency( $currency ) ) { $conversion_rate = 1; } return (float) $amount / $conversion_rate; } /** * Interprets an exchange rate from the Stripe API. * * @param float $exchange_rate The exchange rate returned from the stripe API. * @param string $presentment_currency The currency the customer was charged in. * @param string $base_currency The Stripe account currency. * @return float */ public static function interpret_string_exchange_rate( float $exchange_rate, string $presentment_currency, string $base_currency ): float { $is_presentment_currency_zero_decimal = self::is_zero_decimal_currency( strtolower( $presentment_currency ) ); $is_base_currency_zero_decimal = self::is_zero_decimal_currency( strtolower( $base_currency ) ); if ( $is_presentment_currency_zero_decimal && ! $is_base_currency_zero_decimal ) { return $exchange_rate / 100; } elseif ( ! $is_presentment_currency_zero_decimal && $is_base_currency_zero_decimal ) { return $exchange_rate * 100; } else { return $exchange_rate; } } /** * Check whether a given currency is in the list of zero-decimal currencies supported by Stripe. * * @param string $currency The currency code. * * @return bool */ public static function is_zero_decimal_currency( string $currency ): bool { if ( in_array( strtolower( $currency ), self::zero_decimal_currencies(), true ) ) { return true; } return false; } /** * List of currencies supported by Stripe, the amounts for which are already in the smallest unit. * Sourced directly from https://stripe.com/docs/currencies#zero-decimal * * @return string[] */ public static function zero_decimal_currencies(): array { return [ strtolower( Currency_Code::BURUNDIAN_FRANC ), // Burundian Franc. strtolower( Currency_Code::CHILEAN_PESO ), // Chilean Peso. strtolower( Currency_Code::DJIBOUTIAN_FRANC ), // Djiboutian Franc. strtolower( Currency_Code::GUINEAN_FRANC ), // Guinean Franc. strtolower( Currency_Code::JAPANESE_YEN ), // Japanese Yen. strtolower( Currency_Code::COMORIAN_FRANC ), // Comorian Franc. strtolower( Currency_Code::SOUTH_KOREAN_WON ), // South Korean Won. strtolower( Currency_Code::MALAGASY_ARIARY ), // Malagasy Ariary. strtolower( Currency_Code::PARAGUAYAN_GUARANI ), // Paraguayan Guaraní. strtolower( Currency_Code::RWANDAN_FRANC ), // Rwandan Franc. strtolower( Currency_Code::VIETNAMESE_DONG ), // Vietnamese Đồng. strtolower( Currency_Code::VANUATU_VATU ), // Vanuatu Vatu. strtolower( Currency_Code::CENTRAL_AFRICAN_CFA_FRANC ), // Central African CFA Franc. strtolower( Currency_Code::WEST_AFRICAN_CFA_FRANC ), // West African CFA Franc. strtolower( Currency_Code::CFP_FRANC ), // CFP Franc. ]; } /** * List of countries enabled for Stripe platform account. See also this URL: * https://woocommerce.com/document/woopayments/compatibility/countries/#supported-countries * * @return string[] */ public static function supported_countries(): array { return [ Country_Code::UNITED_ARAB_EMIRATES => __( 'United Arab Emirates', 'woocommerce-payments' ), Country_Code::AUSTRIA => __( 'Austria', 'woocommerce-payments' ), Country_Code::AUSTRALIA => __( 'Australia', 'woocommerce-payments' ), Country_Code::BELGIUM => __( 'Belgium', 'woocommerce-payments' ), Country_Code::BULGARIA => __( 'Bulgaria', 'woocommerce-payments' ), Country_Code::CANADA => __( 'Canada', 'woocommerce-payments' ), Country_Code::SWITZERLAND => __( 'Switzerland', 'woocommerce-payments' ), Country_Code::CYPRUS => __( 'Cyprus', 'woocommerce-payments' ), Country_Code::CZECHIA => __( 'Czech Republic', 'woocommerce-payments' ), Country_Code::GERMANY => __( 'Germany', 'woocommerce-payments' ), Country_Code::DENMARK => __( 'Denmark', 'woocommerce-payments' ), Country_Code::ESTONIA => __( 'Estonia', 'woocommerce-payments' ), Country_Code::FINLAND => __( 'Finland', 'woocommerce-payments' ), Country_Code::SPAIN => __( 'Spain', 'woocommerce-payments' ), Country_Code::FRANCE => __( 'France', 'woocommerce-payments' ), Country_Code::CROATIA => __( 'Croatia', 'woocommerce-payments' ), Country_Code::JAPAN => __( 'Japan', 'woocommerce-payments' ), Country_Code::LUXEMBOURG => __( 'Luxembourg', 'woocommerce-payments' ), Country_Code::UNITED_KINGDOM => __( 'United Kingdom (UK)', 'woocommerce-payments' ), Country_Code::GREECE => __( 'Greece', 'woocommerce-payments' ), Country_Code::HONG_KONG => __( 'Hong Kong', 'woocommerce-payments' ), Country_Code::HUNGARY => __( 'Hungary', 'woocommerce-payments' ), Country_Code::IRELAND => __( 'Ireland', 'woocommerce-payments' ), Country_Code::ITALY => __( 'Italy', 'woocommerce-payments' ), Country_Code::LITHUANIA => __( 'Lithuania', 'woocommerce-payments' ), Country_Code::LATVIA => __( 'Latvia', 'woocommerce-payments' ), Country_Code::MALTA => __( 'Malta', 'woocommerce-payments' ), Country_Code::NETHERLANDS => __( 'Netherlands', 'woocommerce-payments' ), Country_Code::NORWAY => __( 'Norway', 'woocommerce-payments' ), Country_Code::NEW_ZEALAND => __( 'New Zealand', 'woocommerce-payments' ), Country_Code::POLAND => __( 'Poland', 'woocommerce-payments' ), Country_Code::PORTUGAL => __( 'Portugal', 'woocommerce-payments' ), Country_Code::ROMANIA => __( 'Romania', 'woocommerce-payments' ), Country_Code::SWEDEN => __( 'Sweden', 'woocommerce-payments' ), Country_Code::SLOVENIA => __( 'Slovenia', 'woocommerce-payments' ), Country_Code::SLOVAKIA => __( 'Slovakia', 'woocommerce-payments' ), Country_Code::SINGAPORE => __( 'Singapore', 'woocommerce-payments' ), Country_Code::UNITED_STATES => __( 'United States (US)', 'woocommerce-payments' ), Country_Code::PUERTO_RICO => __( 'Puerto Rico', 'woocommerce-payments' ), ]; } /** * Verifies whether a certain ZIP code is valid for the US, incl. 4-digit extensions. * * @param string $zip The ZIP code to verify. * @return boolean */ public static function is_valid_us_zip_code( $zip ) { return ! empty( $zip ) && preg_match( '/^\d{5,5}(-\d{4,4})?$/', $zip ); } /** * Returns the charge_id for an "Order #" search term * or all charge_ids for a "Subscription #" search term. * * @param string $term Search term. * * @return array The charge_id(s) for the order or subscription. */ public static function get_charge_ids_from_search_term( $term ) { $order_term = __( 'Order #', 'woocommerce-payments' ); if ( substr( $term, 0, strlen( $order_term ) ) === $order_term ) { $term_parts = explode( $order_term, $term, 2 ); $order_id = isset( $term_parts[1] ) ? $term_parts[1] : ''; $order = wc_get_order( $order_id ); if ( $order ) { return [ $order->get_meta( '_charge_id' ) ]; } } $subscription_term = __( 'Subscription #', 'woocommerce-payments' ); if ( function_exists( 'wcs_get_subscription' ) && substr( $term, 0, strlen( $subscription_term ) ) === $subscription_term ) { $term_parts = explode( $subscription_term, $term, 2 ); $subscription_id = isset( $term_parts[1] ) ? $term_parts[1] : ''; $subscription = wcs_get_subscription( $subscription_id ); if ( $subscription ) { return array_map( function ( $order ) { return $order->get_meta( '_charge_id' ); }, $subscription->get_related_orders( 'all' ) ); } } return []; } /** * Swaps "Order #" and "Subscription #" search terms with available charge_ids. * * @param array $search Raw search query terms. * * @return array Processed search strings. */ public static function map_search_orders_to_charge_ids( $search ) { // Map Order # and Subscription # terms to the actual charge IDs to be used in the server. $terms = []; foreach ( $search as $term ) { $charge_ids = self::get_charge_ids_from_search_term( $term ); if ( ! empty( $charge_ids ) ) { foreach ( $charge_ids as $charge_id ) { $terms[] = $charge_id; } } else { $terms[] = $term; } } return $terms; } /** * Redacts the provided array, removing the sensitive information, and limits its depth to LOG_MAX_RECURSION. * * @param object|array $input The array to redact. * @param array $keys_to_redact The keys whose values need to be redacted. * @param integer $level The current recursion level. * * @return string|array The redacted array. */ public static function redact_array( $input, array $keys_to_redact, int $level = 0 ) { if ( is_object( $input ) ) { // TODO: if we ever want to log objects, they could implement a method returning an array or a string. return get_class( $input ) . '()'; } if ( ! is_array( $input ) ) { return $input; } if ( $level >= self::MAX_ARRAY_DEPTH ) { return '(recursion limit reached)'; } $result = []; foreach ( $input as $key => $value ) { if ( in_array( $key, $keys_to_redact, true ) ) { $result[ $key ] = '(redacted)'; continue; } $result[ $key ] = self::redact_array( $value, $keys_to_redact, $level + 1 ); } return $result; } /** * Apply a callback on every value in an array, regardless of the number of array dimensions. * * @param array $input The array to map. * @param callable $callback The callback to apply. * * @return array The mapped array. */ public static function array_map_recursive( array $input, callable $callback ): array { foreach ( $input as $key => $value ) { if ( \is_array( $value ) ) { $value = self::array_map_recursive( $value, $callback ); } else { $value = $callback( $value, $key, $input ); } $input[ $key ] = $value; } return $input; } /** * Filter a multidimensional array. * * It works just like array_filter, but it also filters multidimensional/nested arrays, regardless of depth. * * @see https://www.php.net/manual/en/function.array-filter.php * * @param array $input The array to filter. * @param callable|null $callback Optional. The callback to apply. * The callback should return true to keep the value, false otherwise. * If no callback is provided, all non-truthy values will be removed. * * @return array The filtered array. */ public static function array_filter_recursive( array $input, ?callable $callback = null ): array { foreach ( $input as $key => &$value ) { // Mind the use of a reference. if ( \is_array( $value ) ) { $value = self::array_filter_recursive( $value, $callback ); if ( ! $value ) { unset( $input[ $key ] ); } } elseif ( ! is_null( $callback ) ) { if ( ! $callback( $value ) ) { unset( $input[ $key ] ); } } elseif ( ! $value ) { unset( $input[ $key ] ); } } unset( $value ); // Kill the reference to avoid memory leaks. return $input; } /** * Merge arrays recursively like array_merge. * * This method merges any number of arrays recursively, replacing entries with string keys with values from latter arrays. * If the entry or the next value to be assigned is an array, then it automagically treats both arguments as an array. * Numeric entries are appended, not replaced, but only if they are unique. * If the entry or the next value to be assigned is null, it will not overwrite non-null entries. * * Note that this does not work the same as array_merge_recursive: * array_merge_recursive has a behavior that is not quite helpful, especially around overwriting values * with the same string keys (it will not overwrite, but gather them in an array). * * @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201 (initial source) * * @return array */ public static function array_merge_recursive_distinct(): array { $arrays = func_get_args(); $base = array_shift( $arrays ); // Make sure the base is an array. if ( ! is_array( $base ) ) { $base = empty( $base ) ? [] : [ $base ]; } foreach ( $arrays as $append ) { // Coerce single values to array. if ( ! is_array( $append ) ) { $append = [ $append ]; } foreach ( $append as $key => $value ) { if ( ! array_key_exists( $key, $base ) && ! is_numeric( $key ) ) { $base[ $key ] = $value; continue; } // We include null values only when using string keys that don't exist in the base. // For the rest of the scenarios, null entries are ignored. if ( is_null( $value ) ) { continue; } if ( is_array( $value ) || ( array_key_exists( $key, $base ) && is_array( $base[ $key ] ) ) ) { if ( ! isset( $base[ $key ] ) ) { $base[ $key ] = []; } $base[ $key ] = self::array_merge_recursive_distinct( $base[ $key ], $value ); } elseif ( is_numeric( $key ) ) { if ( ! in_array( $value, $base, true ) ) { $base[] = $value; } } else { $base[ $key ] = $value; } } } return $base; } /** * Gets order intent currency from meta data or order currency. * * @param WC_Order $order The order whose intent currency we want to get. * * @return string The currency. */ public static function get_order_intent_currency( WC_Order $order ): string { $intent_currency = $order->get_meta( self::ORDER_INTENT_CURRENCY_META_KEY ); if ( ! empty( $intent_currency ) ) { return $intent_currency; } return $order->get_currency(); } /** * Saves intent currency in order meta data. * * @param WC_Order $order The order whose intent currency we want to set. * @param string $currency The intent currency. */ public static function set_order_intent_currency( WC_Order $order, string $currency ) { $order->update_meta_data( self::ORDER_INTENT_CURRENCY_META_KEY, $currency ); } /** * Checks if the currently displayed page is the WooPayments * settings page or a payment method settings page. * * @return bool */ public static function is_payments_settings_page(): bool { global $current_section, $current_tab; return ( is_admin() && $current_tab && $current_section && 'checkout' === $current_tab && 0 === strpos( $current_section, 'woocommerce_payments' ) ); } /** * Checks if the currently displayed page is the WooPayments onboarding page. * * @return bool */ public static function is_onboarding_page(): bool { return ( is_admin() && isset( $_GET['page'] ) && 'wc-admin' === $_GET['page'] // phpcs:ignore WordPress.Security.NonceVerification && isset( $_GET['path'] ) && '/payments/onboarding' === $_GET['path'] // phpcs:ignore WordPress.Security.NonceVerification ); } /** * Converts a 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 $locale The locale to convert. * * @return string Closest locale supported by Stripe ('auto' if NONE) */ public static function convert_to_stripe_locale( string $locale ): string { // 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). 'fil', // Filipino (Philippines). 'fr', // French (France). 'fr-CA', // French (Canada). 'he', // Hebrew (Israel). 'hr', // Croatian (Croatia). 'hu', // Hungarian (Hungary). 'id', // Indonesian (Indonesia). 'it', // Italian (Italy). 'ja', // Japanese (Japan). 'ko', // Korean (Korea). '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 (Thailand). 'tr', // Turkish (Turkey). 'vi', // Vietnamese (Vietnam). '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( '_', '-', $locale ); if ( in_array( $locale, $supported, true ) ) { return $locale; } // For the Latin America and Caribbean region Stripe uses the locale. // For now we only support Spanish (Spain) in the extension, if/when support for Latin America and the Caribbean // locales is added we will need to group all locales for 'UN M49' under 'es_419' (52 countries in total). // https://en.wikipedia.org/wiki/UN_M49. // Remove the country code and try with that. $base_locale = substr( $locale, 0, 2 ); if ( in_array( $base_locale, $supported, true ) ) { return $base_locale; } // Return 'auto' so Stripe.js uses the browser locale. return 'auto'; } /** * Returns redacted customer-facing error messages for notices. * * This function tries to filter out API exceptions that should not be displayed to customers. * Generally, only Stripe exceptions with type of `card_error` should be displayed. * Other API errors should be redacted (https://stripe.com/docs/api/errors#errors-message). * * @param Exception $e Exception to get the message from. * @param boolean $blocked_by_fraud_rules Whether the payment was blocked by the fraud rules. Defaults to false. * * @return string */ public static function get_filtered_error_message( Exception $e, bool $blocked_by_fraud_rules = false ) { $error_message = method_exists( $e, 'getLocalizedMessage' ) ? $e->getLocalizedMessage() : $e->getMessage(); // These notices can be shown when placing an order or adding a new payment method, so we aim for // more generic messages instead of specific order/payment messages when the API Exception is redacted. if ( $e instanceof Connection_Exception ) { $error_message = __( 'There was an error while processing this request. If you continue to see this notice, please contact the admin.', 'woocommerce-payments' ); } elseif ( $e instanceof Amount_Too_Small_Exception ) { $minimum_amount = $e->get_minimum_amount(); $currency = $e->get_currency(); // Cache the result. static::cache_minimum_amount( $currency, $minimum_amount ); $interpreted_amount = self::interpret_stripe_amount( $minimum_amount, $currency ); $price = wc_price( $interpreted_amount, [ 'currency' => strtoupper( $currency ) ] ); return sprintf( // translators: %s a formatted price. __( 'The selected payment method requires a total amount of at least %s.', 'woocommerce-payments' ), wp_strip_all_tags( html_entity_decode( $price ) ) ); } elseif ( $e instanceof API_Exception && 'amount_too_large' === $e->get_error_code() ) { $error_message = $e->getMessage(); } elseif ( $e instanceof API_Exception && 'wcpay_bad_request' === $e->get_error_code() ) { $error_message = __( 'We\'re not able to process this request. Please refresh the page and try again.', 'woocommerce-payments' ); } elseif ( $e instanceof API_Exception && ! empty( $e->get_error_type() ) && 'card_error' !== $e->get_error_type() ) { $error_message = __( 'We\'re not able to process this request. Please refresh the page and try again.', 'woocommerce-payments' ); } elseif ( $e instanceof API_Exception && 'card_error' === $e->get_error_type() && 'incorrect_zip' === $e->get_error_code() && ! $blocked_by_fraud_rules ) { $error_message = __( 'We couldn’t verify the postal code in your billing address. Make sure the information is current with your card issuing bank and try again.', 'woocommerce-payments' ); } return $error_message; } /** * Returns the customer facing HTTP status codes for an exception. * * @param Exception $e Exception to get the HTTP status code for. * * @return int */ public static function get_filtered_error_status_code( Exception $e ): int { $status_code = null; if ( $e instanceof API_Exception ) { $status_code = $e->get_http_code(); } // Hosting companies might use the 402 status code to return a custom error page. // When 402 is returned by Stripe, let's return 400 instead. // The frontend doesn't make use of the status code. if ( 402 === $status_code ) { $status_code = 400; } return $status_code ?? 400; } /** * Get the BNPL limits per currency for a specific payment method. * * FLAG: PAYMENT_METHODS_LIST * This can be replaced once all BNPL methods are converted to use definitions. * * @param string $payment_method The payment method name ('affirm', 'afterpay_clearpay', or 'klarna'). * @return array The BNPL limits per currency for the specified payment method. */ public static function get_bnpl_limits_per_currency( $payment_method ) { switch ( $payment_method ) { case 'affirm': return [ Currency_Code::CANADIAN_DOLLAR => [ Country_Code::CANADA => [ 'min' => 5000, 'max' => 3000000, ], // Represents CAD 50 - 30,000 CAD. ], Currency_Code::UNITED_STATES_DOLLAR => [ Country_Code::UNITED_STATES => [ 'min' => 5000, 'max' => 3000000, ], ], // Represents USD 50 - 30,000 USD. ]; case 'afterpay_clearpay': return [ Currency_Code::AUSTRALIAN_DOLLAR => [ Country_Code::AUSTRALIA => [ 'min' => 100, 'max' => 200000, ], // Represents AUD 1 - 2,000 AUD. ], Currency_Code::CANADIAN_DOLLAR => [ Country_Code::CANADA => [ 'min' => 100, 'max' => 200000, ], // Represents CAD 1 - 2,000 CAD. ], Currency_Code::NEW_ZEALAND_DOLLAR => [ Country_Code::NEW_ZEALAND => [ 'min' => 100, 'max' => 200000, ], // Represents NZD 1 - 2,000 NZD. ], Currency_Code::POUND_STERLING => [ Country_Code::UNITED_KINGDOM => [ 'min' => 100, 'max' => 120000, ], // Represents GBP 1 - 1,200 GBP. ], Currency_Code::UNITED_STATES_DOLLAR => [ Country_Code::UNITED_STATES => [ 'min' => 100, 'max' => 400000, ], // Represents USD 1 - 4,000 USD. ], ]; case 'klarna': return [ Currency_Code::UNITED_STATES_DOLLAR => [ Country_Code::UNITED_STATES => [ 'min' => 100, 'max' => 1000000, ], // Represents USD 1 - 10,000 USD. ], Currency_Code::POUND_STERLING => [ Country_Code::UNITED_KINGDOM => [ 'min' => 100, 'max' => 500000, ], // Represents GBP 1 - 5,000 GBP. ], Currency_Code::EURO => [ Country_Code::AUSTRIA => [ 'min' => 100, 'max' => 1000000, ], // Represents EUR 1 - 10,000 EUR. Country_Code::BELGIUM => [ 'min' => 100, 'max' => 1000000, ], // Represents EUR 1 - 10,000 EUR. Country_Code::GERMANY => [ 'min' => 100, 'max' => 1000000, ], // Represents EUR 1 - 10,000 EUR. Country_Code::NETHERLANDS => [ 'min' => 100, 'max' => 500000, ], // Represents EUR 1 - 5,000 EUR. Country_Code::FINLAND => [ 'min' => 100, 'max' => 1000000, ], // Represents EUR 1 - 10,000 EUR. Country_Code::SPAIN => [ 'min' => 100, 'max' => 1000000, ], // Represents EUR 1 - 10,000 EUR. Country_Code::IRELAND => [ 'min' => 100, 'max' => 400000, ], // Represents EUR 1 - 4,000 EUR. Country_Code::ITALY => [ 'min' => 100, 'max' => 400000, ], // Represents EUR 1 - 4,000 EUR. Country_Code::FRANCE => [ 'min' => 100, 'max' => 400000, ], // Represents EUR 1 - 4,000 EUR. ], Currency_Code::DANISH_KRONE => [ Country_Code::DENMARK => [ 'min' => 100, 'max' => 10000000, ], // Represents DKK 1 - 100,000 DKK. ], Currency_Code::NORWEGIAN_KRONE => [ Country_Code::NORWAY => [ 'min' => 100, 'max' => 10000000, ], // Represents NOK 1 - 100,000 NOK. ], Currency_Code::SWEDISH_KRONA => [ Country_Code::SWEDEN => [ 'min' => 100, 'max' => 10000000, ], // Represents SEK 1 - 100,000 SEK. ], ]; default: return []; } } /** * Check if any BNPL method is available for a given country, currency, and price. * * @param array $enabled_methods Array of enabled BNPL methods. * @param string $country_code Country code. * @param string $currency_code Currency code. * @param float $price Product price. * @return bool True if any BNPL method is available, false otherwise. */ public static function is_any_bnpl_method_available( array $enabled_methods, string $country_code, string $currency_code, float $price ): bool { $price_in_cents = $price; foreach ( $enabled_methods as $method ) { $limits = self::get_bnpl_limits_per_currency( $method ); if ( isset( $limits[ $currency_code ][ $country_code ] ) ) { $min_amount = $limits[ $currency_code ][ $country_code ]['min']; $max_amount = $limits[ $currency_code ][ $country_code ]['max']; if ( $price_in_cents >= $min_amount && $price_in_cents <= $max_amount ) { return true; } } } return false; } /** * Check if any BNPL method is available for a given country. * * @param array $enabled_methods Array of enabled BNPL methods. * @param string $country_code Country code. * @param string $currency_code Currency code. * @return bool True if any BNPL method is available, false otherwise. */ public static function is_any_bnpl_supporting_country( array $enabled_methods, string $country_code, string $currency_code ): bool { foreach ( $enabled_methods as $method ) { $limits = self::get_bnpl_limits_per_currency( $method ); if ( isset( $limits[ $currency_code ][ $country_code ] ) ) { return true; } } return false; } /** * Saves the minimum amount required for transactions in a given currency. * * @param string $currency The currency. * @param int $amount The minimum amount. */ public static function cache_minimum_amount( $currency, $amount ) { set_transient( 'wcpay_minimum_amount_' . strtolower( $currency ), $amount, DAY_IN_SECONDS ); } /** * Checks if there is a minimum amount required for transactions in a given currency. * * @param string $currency The currency to check for. * * @return int|null Either the minimum amount, or `null` if not available. */ public static function get_cached_minimum_amount( $currency ) { $cached = get_transient( 'wcpay_minimum_amount_' . strtolower( $currency ) ); return (int) $cached ? (int) $cached : null; } /** * Check if order is locked for payment processing * * @param WC_Order $order The order that is being paid. * @param string $intent_id The id of the intent that is being processed. * @return bool A flag that indicates whether the order is already locked. */ public static function is_order_locked( $order, $intent_id = null ) { $order_id = $order->get_id(); $transient_name = 'wcpay_processing_intent_' . $order_id; $processing = get_transient( $transient_name ); // Block the process if the same intent is already being handled. return ( '-1' === $processing || ( isset( $intent_id ) && $processing === $intent_id ) ); } /** * Lock an order for payment intent processing for 5 minutes. * * @param WC_Order $order The order that is being paid. * @param string $intent_id The id of the intent that is being processed. * @return void */ public static function lock_order_payment( $order, $intent_id = null ) { $order_id = $order->get_id(); $transient_name = 'wcpay_processing_intent_' . $order_id; set_transient( $transient_name, empty( $intent_id ) ? '-1' : $intent_id, 5 * MINUTE_IN_SECONDS ); } /** * Unlocks an order for processing by payment intents. * * @param WC_Order $order The order that is being unlocked. */ public static function unlock_order_payment( $order ) { $order_id = $order->get_id(); delete_transient( 'wcpay_processing_intent_' . $order_id ); } /** * Returns the correct id to be used on the transaction URL * The primary ID is prioritized and it fallbacks to the fallback ID * * @param string $primary_id Usually the Payment Intent ID, but can be an order ID. * @param string $fallback_id Usually the Charge ID. * * @return string */ public static function get_transaction_url_id( $primary_id, $fallback_id ) { return ! empty( $primary_id ) ? $primary_id : $fallback_id; } /** * Composes url for transaction details page. * * @param string $primary_id Usually the Payment Intent ID, but can be an order ID. * @param string $fallback_id Usually the Charge ID. * @param array $query_args Optional additonal query args to append to the URL. * * @return string Transaction details page url. */ public static function compose_transaction_url( $primary_id, $fallback_id, $query_args = [] ) { if ( empty( $fallback_id ) && empty( $primary_id ) ) { return ''; } if ( strpos( $primary_id, 'seti_' ) !== false ) { return ''; } return add_query_arg( // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in. array_merge( [ 'page' => 'wc-admin', 'path' => '/payments/transactions/details', 'id' => self::get_transaction_url_id( $primary_id, $fallback_id ), ], $query_args ), admin_url( 'admin.php' ) ); } /** * Retrieve last WC refund from order ID. * * @param int $order_id WC Order ID. * * @return null|WC_Order_Refund */ public static function get_last_refund_from_order_id( $order_id ) { $wc_refunds = wc_get_orders( [ 'type' => 'shop_order_refund', 'parent' => $order_id, 'limit' => 1, 'orderby' => 'ID', 'order' => 'DESC', ] ); if ( is_array( $wc_refunds ) && ! empty( $wc_refunds ) && is_a( $wc_refunds[0], WC_Order_Refund::class ) ) { return $wc_refunds[0]; } return null; } /** * Helper function to check whether to show default new onboarding flow or as an exception disable it (if specific constant is set) . * * @deprecated 9.8.0 There is no longer an optional "new" onboarding flow, so this method is no longer needed. * * @return boolean */ public static function should_use_new_onboarding_flow(): bool { if ( apply_filters( 'wcpay_disable_new_onboarding', defined( 'WCPAY_DISABLE_NEW_ONBOARDING' ) && WCPAY_DISABLE_NEW_ONBOARDING ) ) { return false; } return true; } /** * Checks whether the Force disconnected option is enabled. * * @return bool */ public static function force_disconnected_enabled(): bool { return '1' === get_option( self::FORCE_DISCONNECTED_FLAG_NAME, '0' ); } /** * Return the currency format based on the symbol position. * Similar to get_woocommerce_price_format but with an input. * * @param string $currency_pos currency symbol position. * * @return string The currency format. */ public static function get_woocommerce_price_format( string $currency_pos ): string { $default_left = '%1$s%2$s'; switch ( $currency_pos ) { case 'left': return $default_left; case 'right': return '%2$s%1$s'; case 'left_space': return '%1$s %2$s'; case 'right_space': return '%2$s %1$s'; default: return $default_left; } } /** * Transform the currency format returned from localization service into * the format that can be used by wc_price * * @param string $currency the currency code. * * @return array The currency format. */ public static function get_currency_format_for_wc_price( string $currency ): array { $currency = strtoupper( $currency ); $currency_data = WC_Payments::get_localization_service()->get_currency_format( $currency ); $currency_format_for_wc_price = []; foreach ( $currency_data as $key => $format ) { switch ( $key ) { case 'thousand_sep': $currency_format_for_wc_price['thousand_separator'] = $format; break; case 'decimal_sep': $currency_format_for_wc_price['decimal_separator'] = $format; break; case 'num_decimals': $currency_format_for_wc_price['decimals'] = $format; break; case 'currency_pos': $currency_format_for_wc_price['price_format'] = self::get_woocommerce_price_format( $format ); break; } } $currency_format_for_wc_price['currency'] = $currency; return $currency_format_for_wc_price; } /** * Format an amount according to the given currency format. * * @param float $amount Amount to format. * @param string $currency 3-letter currency code. * * @return string */ public static function format_currency( float $amount, string $currency ): string { $currency = strtoupper( $currency ); $formatted = html_entity_decode( wp_strip_all_tags( wc_price( $amount, self::get_currency_format_for_wc_price( $currency ) ) ) ); return $formatted; } /** * Format amount according to the given currency with the currency code in the right. * * @param float $amount Amount. * @param string $currency 3-letter currency code. * @param bool $skip_symbol Optional. If true, trims off the short currency symbol. Default false. * @param array $currency_format Optional. Additional currency format for wc_price. * * @return string Formatted currency representation */ public static function format_explicit_currency( float $amount, string $currency, bool $skip_symbol = false, array $currency_format = [] ): string { $currency = strtoupper( $currency ); $formatted_amount = wc_price( $amount, wp_parse_args( $currency_format, self::get_currency_format_for_wc_price( $currency ) ) ); $formatted_amount = html_entity_decode( wp_strip_all_tags( $formatted_amount ) ); if ( $skip_symbol ) { $formatted_amount = preg_replace( '/[^0-9,\.]+/', '', $formatted_amount ); } if ( false === strpos( $formatted_amount, $currency ) ) { return $formatted_amount . ' ' . $currency; } return $formatted_amount; } /** * Checks if the HPOS order tables are being used. * * @return bool True if HPOS tables are enabled and being used. */ public static function is_hpos_tables_usage_enabled() { return class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled(); } /** * Get the core request class name as WordPress 6.2 introduces a breaking namespace change. * * @see https://github.com/WordPress/wordpress-develop/commit/d7dd42d72fe5b10460072e7c78d36c130857e427 * * @return string The request class name. */ public static function get_wpcore_request_class(): string { return version_compare( get_bloginfo( 'version' ), '6.2', '>=' ) ? '\\WpOrg\\Requests\\Requests' : '\\Requests'; } /** * Returns a merchant-friendly description of the dispute reason. * * This mapping is duplicated in client/disputes/strings.ts and on Server. * * @param string $reason The dispute reason. * * @return string */ public static function get_dispute_reason_description( string $reason ): string { switch ( $reason ) { case 'bank_cannot_process': return __( 'Bank cannot process', 'woocommerce-payments' ); case 'check_returned': return __( 'Check returned', 'woocommerce-payments' ); case 'credit_not_processed': return __( 'Credit not processed', 'woocommerce-payments' ); case 'customer_initiated': return __( 'Customer initiated', 'woocommerce-payments' ); case 'debit_not_authorized': return __( 'Debit not authorized', 'woocommerce-payments' ); case 'duplicate': return __( 'Duplicate', 'woocommerce-payments' ); case 'fraudulent': return __( 'Transaction unauthorized', 'woocommerce-payments' ); case 'incorrect_account_details': return __( 'Incorrect account details', 'woocommerce-payments' ); case 'insufficient_funds': return __( 'Insufficient funds', 'woocommerce-payments' ); case 'product_not_received': return __( 'Product not received', 'woocommerce-payments' ); case 'product_unacceptable': return __( 'Product unacceptable', 'woocommerce-payments' ); case 'subscription_canceled': return __( 'Subscription canceled', 'woocommerce-payments' ); case 'unrecognized': return __( 'Unrecognized', 'woocommerce-payments' ); case 'noncompliant': return __( 'Non-compliant', 'woocommerce-payments' ); default: case 'general': return __( 'General', 'woocommerce-payments' ); } } /** * Register a style for use. * * @uses wp_register_style() * @param string $handle Name of the stylesheet. Should be unique. * @param string $path Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. * @param string[] $deps An array of registered stylesheet handles this stylesheet depends on. * @param string $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. * @param boolean $has_rtl If has RTL version to load too. */ public static function register_style( $handle, $path, $deps = [], $version = WC_VERSION, $media = 'all', $has_rtl = true ) { wp_register_style( $handle, $path, $deps, $version, $media ); if ( $has_rtl ) { wp_style_add_data( $handle, 'rtl', 'replace' ); } } /** * Register and enqueue a styles for use. * * @uses wp_enqueue_style() * @param string $handle Name of the stylesheet. Should be unique. * @param string $path Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. * @param string[] $deps An array of registered stylesheet handles this stylesheet depends on. * @param string $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. * @param boolean $has_rtl If has RTL version to load too. */ public static function enqueue_style( $handle, $path = '', $deps = [], $version = WC_VERSION, $media = 'all', $has_rtl = true ) { if ( '' !== $path ) { self::register_style( $handle, $path, $deps, $version, $media, $has_rtl ); } wp_enqueue_style( $handle ); } /** * Check if the current page is the cart page. * * @return bool True if the current page is the cart page, false otherwise. */ public static function is_cart_page(): bool { return is_cart() || has_block( 'woocommerce/cart' ); } /** * Determine if the current page is a cart block. * * @return bool True if the current page is a cart block, false otherwise. */ public static function is_cart_block(): bool { return has_block( 'woocommerce/cart' ); } /** * Determine if the request that's currently being processed is a Store API request. * * @return bool True if request is a Store API request, false otherwise. */ public static function is_store_api_request(): bool { // @TODO We should move to a more robust way of getting to the route, like WC is doing in the StoreAPI library. https://github.com/woocommerce/woocommerce/blob/9ac48232a944baa2dbfaa7dd47edf9027cca9519/plugins/woocommerce/src/StoreApi/Authentication.php#L15-L15 if ( isset( $_REQUEST['rest_route'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $rest_route = sanitize_text_field( $_REQUEST['rest_route'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.NonceVerification } else { $rest_route = self::extract_rest_route_from_url(); } // Bail early if the rest route is empty. if ( empty( $rest_route ) ) { return false; } // Try to match the rest route against the store API route patterns. foreach ( self::STORE_API_ROUTE_PATTERNS as $pattern ) { if ( 1 === preg_match( $pattern, $rest_route ) ) { return true; } } // If no match was found, this is not a Store API request. return false; } /** * Extract the REST route from the current request URL. * * @return string The REST route, or empty string if not found. */ private static function extract_rest_route_from_url(): string { // Extract the request path from the request URL. $url_parts = wp_parse_url( esc_url_raw( $_SERVER['REQUEST_URI'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash if ( empty( $url_parts['path'] ) ) { return ''; } $request_path = rtrim( $url_parts['path'], '/' ); if ( empty( $request_path ) ) { return ''; } // Remove the REST API prefix from the request path to end up with the route. $rest_prefix = trailingslashit( rest_get_url_prefix() ); // For multisite subdirectory setups, we need to handle the subdirectory prefix. // Look for the wp-json prefix in the path and extract everything after it. $wp_json_pos = strpos( $request_path, '/' . rtrim( $rest_prefix, '/' ) ); if ( false !== $wp_json_pos ) { return substr( $request_path, $wp_json_pos + strlen( $rest_prefix ) ); } // Fallback: simple prefix replacement for non-multisite cases. return str_replace( $rest_prefix, '', $request_path ); } /** * Gets the current active theme transient for a given location * Falls back to 'stripe' if no transients are set. * * @param string $location The theme location. * @param string $context The theme location to fall back to if both transients are set. * @return string */ public static function get_active_upe_theme_transient_for_location( string $location = 'checkout', string $context = 'blocks' ) { $themes = \WC_Payment_Gateway_WCPay::APPEARANCE_THEME_TRANSIENTS; $active_theme = false; // If an invalid location is sent, we fallback to trying $themes[ 'checkout' ][ 'block' ]. if ( ! isset( $themes[ $location ] ) ) { $active_theme = get_transient( $themes['checkout']['blocks'] ); } elseif ( ! isset( $themes[ $location ][ $context ] ) ) { // If the location is valid but the context is invalid, we fallback to trying $themes[ $location ][ 'block' ]. $active_theme = get_transient( $themes[ $location ]['blocks'] ); } else { $active_theme = get_transient( $themes[ $location ][ $context ] ); } // If $active_theme is still false here, that means that $themes[ $location ][ $context ] is not set, so we try $themes[ $location ][ 'classic' ]. if ( ! $active_theme ) { $active_theme = get_transient( $themes[ $location ][ 'blocks' === $context ? 'classic' : 'blocks' ] ); } // If $active_theme is still false here, nothing at the location is set so we'll try all locations. if ( ! $active_theme ) { foreach ( $themes as $location_const => $contexts ) { // We don't need to check the same location again. if ( $location_const === $location ) { continue; } foreach ( $contexts as $context => $transient ) { $active_theme = get_transient( $transient ); if ( $active_theme ) { break 2; // This will break both loops. } } } } // If $active_theme is still false, we don't have any theme set in the transients, so we fallback to 'stripe'. if ( $active_theme ) { return $active_theme; } // Fallback to 'stripe' if no transients are set. return 'stripe'; } /** * Returns the list of countries in the European Economic Area (EEA). * * Based on the list documented at https://www.gov.uk/eu-eea. * * @return string[] */ public static function get_european_economic_area_countries() { return [ Country_Code::AUSTRIA, Country_Code::BELGIUM, Country_Code::BULGARIA, Country_Code::CROATIA, Country_Code::CYPRUS, Country_Code::CZECHIA, Country_Code::DENMARK, Country_Code::ESTONIA, Country_Code::FINLAND, Country_Code::FRANCE, Country_Code::GERMANY, Country_Code::GREECE, Country_Code::HUNGARY, Country_Code::IRELAND, Country_Code::ICELAND, Country_Code::ITALY, Country_Code::LATVIA, Country_Code::LIECHTENSTEIN, Country_Code::LITHUANIA, Country_Code::LUXEMBOURG, Country_Code::MALTA, Country_Code::NORWAY, Country_Code::NETHERLANDS, Country_Code::POLAND, Country_Code::PORTUGAL, Country_Code::ROMANIA, Country_Code::SLOVAKIA, Country_Code::SLOVENIA, Country_Code::SPAIN, Country_Code::SWEDEN, ]; } /** * Log directly to WC logger, bypassing WCPay's logging settings. * * Use for critical errors that should always be captured, or errors that can occur * before WCPay settings are initialized (e.g., onboarding errors). * * @param string $message Log message. * @param string $level Log level: 'emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'. */ public static function log_to_wc( string $message, string $level = 'error' ): void { if ( function_exists( 'wc_get_logger' ) ) { $valid_levels = [ 'emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug' ]; $level = in_array( $level, $valid_levels, true ) ? $level : 'error'; $logger = wc_get_logger(); $logger->$level( $message, [ 'source' => 'woopayments' ] ); } } } includes/class-wc-payments-address-provider.php 0000777 00000007062 15251156605 0015732 0 ustar 00 <?php /** * Class WC_Payments_Address_Provider * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'Automattic\\WooCommerce\\Internal\\AddressProvider\\AbstractAutomatticAddressProvider' ) ) { return; } use Automattic\WooCommerce\Internal\AddressProvider\AbstractAutomatticAddressProvider; use WCPay\Database_Cache; use WCPay\Logger; /** * Address provider implementation for WooCommerce Payments. */ class WC_Payments_Address_Provider extends AbstractAutomatticAddressProvider { /** * Placeholder value to use in the cache when the token retrieval fails. */ const INVALID_TOKEN = 'INVALID_TOKEN'; /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ protected $payments_api_client; /** * Payments account service. * * @var WC_Payments_Account */ private $account; /** * Database cache instance. * * @var Database_Cache */ private $database_cache; /** * Constructor. * * @param WC_Payments_API_Client $payments_api_client The API client for making requests. * @param WC_Payments_Account $account The payments account service. * @param Database_Cache $database_cache The database cache instance. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account, Database_Cache $database_cache ) { $this->id = 'woocommerce_payments'; $this->name = __( 'WooCommerce Payments', 'woocommerce-payments' ); $this->payments_api_client = $payments_api_client; $this->account = $account; $this->database_cache = $database_cache; parent::__construct(); } /** * Checks if the core setting is enabled before loading scripts. * The parent method does not check this (will be patched and this override can be removed when WC 10.4 is released) */ public function load_scripts() { if ( wc_string_to_bool( get_option( 'woocommerce_address_autocomplete_enabled', 'no' ) ) === true ) { parent::load_scripts(); } } /** * Get address service JWT token from the WCPay server. * * This method calls the address-autocomplete-token endpoint to retrieve * a JWT token for address autocomplete services. * * Caching and retries and backoff logic is handled by the parent class, if you must override that without fixing it upstream, you should also override `load_jwt`. * * @return string|WP_Error The JWT token on success, WP_Error on failure. */ public function get_address_service_jwt() { $token = $this->database_cache->get_or_add( Database_Cache::ADDRESS_AUTOCOMPLETE_JWT_KEY, function () { if ( ! $this->account->is_stripe_connected() ) { return self::INVALID_TOKEN; } try { $response = $this->payments_api_client->get_address_autocomplete_token(); return $response['token'] ?? self::INVALID_TOKEN; } catch ( \Exception $e ) { Logger::error( 'Unexpected error getting address service JWT: ' . $e->getMessage() ); return self::INVALID_TOKEN; } }, '__return_true' ); if ( self::INVALID_TOKEN === $token ) { return new WP_Error( 'wcpay_address_service_error', 'An unexpected error occurred while retrieving the address service token.' ); } return $token; } /** * Whether the address provider can send frontend telemetry data. * * @return bool True if telemetry is allowed, false otherwise. */ public function can_telemetry() { // We defer to the global Woo setting. return WC_Site_Tracking::is_tracking_enabled(); } } includes/class-wc-payments-apple-pay-registration.php 0000777 00000023271 15251156605 0017055 0 ustar 00 <?php /** * Class WC_Payments_Apple_Pay_Registration * * Adapted from WooCommerce Stripe Gateway extension. * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; } use WCPay\Logger; use WCPay\Exceptions\API_Exception; use WCPay\Tracker; /** * WC_Payments_Apple_Pay_Registration class. */ class WC_Payments_Apple_Pay_Registration { /** * Client for making requests to the WooCommerce Payments API * * @var WC_Payments_API_Client */ private $payments_api_client; /** * The WCPay account object. * * @var WC_Payments_Account */ private $account; /** * WC_Payment_Gateway_WCPay instance. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * Current domain name. * * @var string */ private $domain_name; /** * Option name for storing Apple Pay domain verification errors. * * @var string */ const APPLE_PAY_DOMAIN_ERROR_OPTION = 'wcpay_apple_pay_domain_error'; /** * Initialize class actions. * * @param WC_Payments_API_Client $payments_api_client WooCommerce Payments API client. * @param WC_Payments_Account $account WooCommerce Payments account. * @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments gateway. */ public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway ) { $this->domain_name = wp_parse_url( get_site_url(), PHP_URL_HOST ); $this->payments_api_client = $payments_api_client; $this->account = $account; $this->gateway = $gateway; } /** * Initializes this class's hooks. * * @return void */ public function init_hooks() { add_action( 'init', [ $this, 'init' ] ); } /** * Initialize hooks. * * @return void */ public function init() { add_action( 'admin_init', [ $this, 'verify_domain_on_domain_name_change' ] ); add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'display_error_notice' ] ); // Listen to Apple Pay gateway settings changes for domain verification. add_action( 'add_option_woocommerce_woocommerce_payments_apple_pay_settings', [ $this, 'verify_domain_on_new_settings' ], 10, 2 ); add_action( 'update_option_woocommerce_woocommerce_payments_apple_pay_settings', [ $this, 'verify_domain_on_updated_settings' ], 10, 2 ); // Also listen to main gateway settings changes, since it's a prerequisite for Apple Pay. add_action( 'update_option_woocommerce_woocommerce_payments_settings', [ $this, 'verify_domain_on_updated_main_gateway_settings' ], 10, 2 ); } /** * Whether Apple Pay is enabled. * * Checks both the main gateway and the Apple Pay gateway are enabled. * * @return bool Whether Apple Pay is enabled. */ private function is_enabled() { // Check if the main gateway is enabled. if ( ! $this->gateway->is_enabled() ) { return false; } // Check if the Apple Pay gateway is enabled. $apple_pay_gateway = WC_Payments::get_payment_gateway_by_id( 'apple_pay' ); if ( ! $apple_pay_gateway ) { return false; } return $apple_pay_gateway->is_enabled(); } /** * Whether Apple Pay was enabled in previous settings. * * @param array|null $prev_settings Apple Pay gateway settings. * * @return bool Whether Apple Pay was enabled. */ private function was_enabled( $prev_settings ) { return 'yes' === ( $prev_settings['enabled'] ?? 'no' ); } /** * Trigger Apple Pay registration upon domain name change. */ public function verify_domain_on_domain_name_change() { $verified_domain = $this->gateway->get_option( 'apple_pay_verified_domain' ); if ( $this->domain_name !== $verified_domain ) { $this->verify_domain_if_configured(); } } /** * Returns the string representation of the current mode. One of: * - 'dev' * - 'test' * - 'live' * * @return string A string representation of the current mode. */ private function get_gateway_mode_string() { if ( WC_Payments::mode()->is_dev() ) { return 'dev'; } elseif ( WC_Payments::mode()->is_test() ) { return 'test'; } return 'live'; } /** * Processes the Stripe domain registration. */ public function register_domain() { $error = null; try { $registration_response = $this->payments_api_client->register_domain( $this->domain_name ); if ( isset( $registration_response['id'] ) && ( isset( $registration_response['apple_pay']['status'] ) && 'active' === $registration_response['apple_pay']['status'] ) ) { $this->gateway->update_option( 'apple_pay_verified_domain', $this->domain_name ); $this->gateway->update_option( 'apple_pay_domain_set', 'yes' ); delete_option( self::APPLE_PAY_DOMAIN_ERROR_OPTION ); Logger::log( __( 'Your domain has been verified with Apple Pay!', 'woocommerce-payments' ) ); Tracker::track_admin( 'wcpay_apple_pay_domain_registration_success', [ 'domain' => $this->domain_name, 'mode' => $this->get_gateway_mode_string(), ] ); return; } elseif ( isset( $registration_response['apple_pay']['status_details']['error_message'] ) ) { $error = $registration_response['apple_pay']['status_details']['error_message']; } } catch ( API_Exception $e ) { $error = $e->getMessage(); } $this->gateway->update_option( 'apple_pay_verified_domain', $this->domain_name ); $this->gateway->update_option( 'apple_pay_domain_set', 'no' ); update_option( self::APPLE_PAY_DOMAIN_ERROR_OPTION, $error ); Logger::log( 'Error registering domain with Apple: ' . $error ); Tracker::track_admin( 'wcpay_apple_pay_domain_registration_failure', [ 'domain' => $this->domain_name, 'reason' => $error, 'mode' => $this->get_gateway_mode_string(), ] ); } /** * Process the Apple Pay domain verification if proper settings are configured. */ public function verify_domain_if_configured() { // If Express Checkout Buttons are not enabled, // do not attempt to register the domain. if ( ! $this->is_enabled() ) { return; } // Register the domain. $this->register_domain(); } /** * Conditionally process the Apple Pay domain verification after settings are initially set. * * @param string $option Option name. * @param array $settings Settings array. */ public function verify_domain_on_new_settings( $option, $settings ) { $this->verify_domain_on_updated_settings( [], $settings ); } /** * Conditionally process the Apple Pay domain verification after settings are updated. * * @param array $prev_settings Settings before update. * @param array $settings Settings after update. */ public function verify_domain_on_updated_settings( $prev_settings, $settings ) { // If Apple Pay wasn't enabled, then might need to verify now. if ( ! $this->was_enabled( $prev_settings ) ) { $this->verify_domain_if_configured(); } } /** * Conditionally process the Apple Pay domain verification after main gateway settings are updated. * * @param array $prev_settings Settings before update. * @param array $settings Settings after update. */ public function verify_domain_on_updated_main_gateway_settings( $prev_settings, $settings ) { $was_main_gateway_enabled = 'yes' === ( $prev_settings['enabled'] ?? 'no' ); // If main gateway wasn't enabled before, might need to verify now. if ( ! $was_main_gateway_enabled ) { $this->verify_domain_if_configured(); } } /** * Display Apple Pay registration errors. */ public function display_error_notice() { if ( ! $this->is_enabled() || ! $this->account->get_is_live() ) { return; } $domain_set = $this->gateway->get_option( 'apple_pay_domain_set' ); $error_notice = get_option( self::APPLE_PAY_DOMAIN_ERROR_OPTION, '' ); $empty_notice = empty( $error_notice ); // Don't display error notice if verification notice is empty and // apple_pay_domain_set option equals to '' or 'yes'. if ( $empty_notice && 'no' !== $domain_set ) { return; } // Clear the error after retrieving it so it only displays once. if ( ! $empty_notice ) { delete_option( self::APPLE_PAY_DOMAIN_ERROR_OPTION ); } /** * Apple pay is enabled by default and domain verification initializes * when setting screen is displayed. So if domain verification is not set, * something went wrong so lets notify user. */ $allowed_html = [ 'a' => [ 'href' => [], 'title' => [], ], ]; $verification_failed = $empty_notice ? __( 'Apple Pay domain verification failed.', 'woocommerce-payments' ) : __( 'Apple Pay domain verification failed with the following error:', 'woocommerce-payments' ); $check_log_text = WC_Payments_Utils::esc_interpolated_html( /* translators: a: Link to the logs page */ __( 'Please check the <a>logs</a> for more details on this issue. Debug log must be enabled under <strong>Advanced settings</strong> to see recorded logs.', 'woocommerce-payments' ), [ 'a' => '<a href="' . admin_url( 'admin.php?page=wc-status&tab=logs' ) . '">', 'strong' => '<strong>', ] ); $learn_more_text = WC_Payments_Utils::esc_interpolated_html( __( '<a>Learn more</a>.', 'woocommerce-payments' ), [ 'a' => '<a href="https://woocommerce.com/document/woopayments/payment-methods/apple-pay/#domain-registration" target="_blank">', ] ); ?> <div class="notice notice-error apple-pay-message"> <p> <strong><?php esc_html_e( 'Express checkouts:', 'woocommerce-payments' ); ?></strong> <?php echo esc_html( $verification_failed ); ?> <?php echo $learn_more_text; /* @codingStandardsIgnoreLine */ ?> </p> <?php if ( ! $empty_notice ) : ?> <p><i><?php echo wp_kses( make_clickable( esc_html( $error_notice ) ), $allowed_html ); ?></i></p> <?php endif; ?> <p><?php echo $check_log_text; /* @codingStandardsIgnoreLine */ ?></p> </div> <?php } } includes/class-wc-payments-dependency-service.php 0000777 00000030720 15251156605 0016226 0 ustar 00 <?php /** * WC_Payments_Dependency_Service class * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Database_Cache; /** * Validates dependencies (core, plugins, versions) for WCPAY * Used in the plugin main class for validation. */ class WC_Payments_Dependency_Service { const WOOCORE_NOT_FOUND = 'woocore_disabled'; const WOOCORE_INCOMPATIBLE = 'woocore_outdated'; const WOOADMIN_NOT_FOUND = 'wc_admin_not_found'; const WOOADMIN_INCOMPATIBLE = 'wc_admin_outdated'; const WP_INCOMPATIBLE = 'wp_outdated'; const DEV_ASSETS_NOT_BUILT = 'dev_assets_not_built'; /** * Initializes this class's WP hooks. * * @return void */ public function init_hooks() { add_filter( 'admin_notices', [ $this, 'display_admin_notices' ] ); } /** * Checks if all the dependencies needed to run WooPayments are present * * @return bool True if all required dependencies are met. */ public function has_valid_dependencies() { if ( defined( 'WCPAY_TEST_ENV' ) && WCPAY_TEST_ENV ) { return true; } return empty( $this->get_invalid_dependencies( true ) ); } /** * Render admin notices for unmet dependencies. Called on the admin_notices hook. * * @return void */ public function display_admin_notices() { // Do not show alerts while installing plugins. if ( self::is_at_plugin_install_page() ) { return; } // Show a message when assets are not built in a dev build. if ( ! $this->are_assets_built() ) { WC_Payments::display_admin_error( $this->get_notice_for_invalid_dependency( self::DEV_ASSETS_NOT_BUILT ) ); } $invalid_dependencies = $this->get_invalid_dependencies(); if ( ! empty( $invalid_dependencies ) ) { WC_Payments::display_admin_error( $this->get_notice_for_invalid_dependency( $invalid_dependencies[0] ) ); } } /** * Returns an array of invalid dependencies * * @param bool $check_account_connection - if should bypass dependency version validation when an account is connected. * * @return array of invalid dependencies as string constants. */ public function get_invalid_dependencies( bool $check_account_connection = false ) { $invalid_dependencies = []; // Either ignore the account connection check or check if there's a cached account connection. $ignore_when_account_is_connected = $check_account_connection && self::has_cached_account_connection(); if ( ! $this->is_woo_core_active() ) { $invalid_dependencies[] = self::WOOCORE_NOT_FOUND; } if ( ! $ignore_when_account_is_connected && ! $this->is_woo_core_version_compatible() ) { $invalid_dependencies[] = self::WOOCORE_INCOMPATIBLE; } if ( ! $this->is_wc_admin_enabled() ) { $invalid_dependencies[] = self::WOOADMIN_NOT_FOUND; } if ( ! $ignore_when_account_is_connected && ! $this->is_wc_admin_version_compatible() ) { $invalid_dependencies[] = self::WOOADMIN_INCOMPATIBLE; } if ( ! $ignore_when_account_is_connected && ! $this->is_wp_version_compatible() ) { $invalid_dependencies[] = self::WP_INCOMPATIBLE; } return $invalid_dependencies; } /** * Checks if WooCommerce is installed and activated. * * @return bool True if WooCommerce is installed and activated. */ public function is_woo_core_active() { // Check if WooCommerce is installed and active. return class_exists( 'WooCommerce' ); } /** * Checks if the version of WooCommerce is compatible with WooPayments. * * @return bool True if WooCommerce version is greater than or equal the minimum accepted */ public function is_woo_core_version_compatible() { $plugin_headers = WC_Payments::get_plugin_headers(); $wc_version = $plugin_headers['WCRequires']; // Check if the version of WooCommerce is compatible with WooPayments. return ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, $wc_version, '>=' ) ); } /** * Checks if the WooCommerce version has WooCommerce Admin bundled (WC 4.0+) * but it's disabled using a filter. * * @return bool True if WC Admin is found */ public function is_wc_admin_enabled() { // Check if the current WooCommerce version has WooCommerce Admin bundled (WC 4.0+) but it's disabled using a filter. if ( ! defined( 'WC_ADMIN_VERSION_NUMBER' ) || apply_filters( 'woocommerce_admin_disabled', false ) ) { return false; } return true; } /** * Checks if the version of WC Admin is compatible with WooPayments. * * @return bool True if WC Admin version is greater than or equal the minimum accepted */ public function is_wc_admin_version_compatible() { // Check if the version of WooCommerce Admin is compatible with WooPayments. return ( defined( 'WC_ADMIN_VERSION_NUMBER' ) && version_compare( WC_ADMIN_VERSION_NUMBER, WCPAY_MIN_WC_ADMIN_VERSION, '>=' ) ); } /** * Checks if the version of WordPress is compatible with WooPayments. * * @return bool True if WordPress version is greater than or equal the minimum accepted */ public function is_wp_version_compatible() { $plugin_headers = WC_Payments::get_plugin_headers(); $wp_version = $plugin_headers['RequiresWP']; return version_compare( get_bloginfo( 'version' ), $wp_version, '>=' ); } /** * Checks some of the asset files to confirm scripts and styles have been correctly built. * * @return bool TRUE if assets have been built or FALSE otherwise. */ public function are_assets_built() { return ( file_exists( WCPAY_ABSPATH . 'dist/index.js' ) && file_exists( WCPAY_ABSPATH . 'dist/index.css' ) ); } /** * Get the error constant of an invalid dependency, and transforms it into HTML to be used in an Admin Notice. * * @param string $code - invalid dependency constant. * * @return string HTML to render admin notice for the unmet dependency. */ private function get_notice_for_invalid_dependency( $code ) { $plugin_headers = WC_Payments::get_plugin_headers(); $wp_version = $plugin_headers['RequiresWP']; $wc_version = $plugin_headers['WCRequires']; $error_message = ''; switch ( $code ) { case self::WOOCORE_NOT_FOUND: $error_message = WC_Payments_Utils::esc_interpolated_html( sprintf( /* translators: %1$s: WooPayments, %2$s: WooCommerce */ __( '%1$s requires <a>%2$s</a> to be installed and active.', 'woocommerce-payments' ), 'WooPayments', 'WooCommerce' ), [ 'a' => '<a href="https://wordpress.org/plugins/woocommerce">' ] ); if ( current_user_can( 'install_plugins' ) ) { if ( is_wp_error( validate_plugin( 'woocommerce/woocommerce.php' ) ) ) { // WooCommerce is not installed. $activate_url = wp_nonce_url( admin_url( 'update.php?action=install-plugin&plugin=woocommerce' ), 'install-plugin_woocommerce' ); $activate_text = __( 'Install WooCommerce', 'woocommerce-payments' ); } else { // WooCommerce is installed, so it just needs to be enabled. $activate_url = wp_nonce_url( admin_url( 'plugins.php?action=activate&plugin=woocommerce/woocommerce.php' ), 'activate-plugin_woocommerce/woocommerce.php' ); $activate_text = __( 'Activate WooCommerce', 'woocommerce-payments' ); } $error_message .= ' <a href="' . $activate_url . '">' . $activate_text . '</a>'; } break; case self::WOOCORE_INCOMPATIBLE: $error_message = WC_Payments_Utils::esc_interpolated_html( sprintf( /* translators: %1: WooPayments, %2: current WooCommerce Payment version, %3: WooCommerce, %4: required WC version number, %5: currently installed WC version number */ __( '%1$s %2$s requires <strong>%3$s %4$s</strong> or greater to be installed (you are using %5$s). ', 'woocommerce-payments' ), 'WooPayments', WCPAY_VERSION_NUMBER, 'WooCommerce', $wc_version, WC_VERSION ), [ 'strong' => '<strong>' ] ); if ( current_user_can( 'update_plugins' ) ) { // Take the user to the "plugins" screen instead of trying to update WooCommerce inline. WooCommerce adds important information // on its plugin row regarding the currently installed extensions and their compatibility with the latest WC version. $error_message .= '<br/>' . WC_Payments_Utils::esc_interpolated_html( sprintf( /* translators: %1$s: WooCommerce, %2$s: WooPayments, a1: link to the Plugins page, a2: link to the page having all previous versions */ __( '<a1>Update %1$s</a1> <strong>(recommended)</strong> or manually re-install <a2>a previous version</a2> of %2$s.', 'woocommerce-payments' ), 'WooCommerce', 'WooPayments' ), [ 'a1' => '<a href="' . admin_url( 'plugins.php' ) . '">', 'strong' => '<strong>', 'a2' => '<a href="https://wordpress.org/plugins/woocommerce-payments/advanced/#download-previous-link" target="_blank">', ] ); } break; case self::WOOADMIN_NOT_FOUND: $error_message = WC_Payments_Utils::esc_interpolated_html( sprintf( /* translators: %1$s: WooPayments, %2$s: WooCommerce Admin */ __( '%1$s requires %2$s to be enabled. Please remove the <code>woocommerce_admin_disabled</code> filter to use %1$s.', 'woocommerce-payments' ), 'WooPayments', 'WooCommerce Admin' ), [ 'code' => '<code>' ] ); break; case self::WOOADMIN_INCOMPATIBLE: $error_message = WC_Payments_Utils::esc_interpolated_html( sprintf( /* translators: %1: WooPayments, %2: WooCommerce Admin, %3: required WC-Admin version number, %4: currently installed WC-Admin version number */ __( '%1$s requires <strong>%2$s %3$s</strong> or greater to be installed (you are using %4$s).', 'woocommerce-payments' ), 'WooPayments', 'WooCommerce Admin', WCPAY_MIN_WC_ADMIN_VERSION, WC_ADMIN_VERSION_NUMBER ), [ 'strong' => '<strong>' ] ); // Let's assume for now that any WC-Admin version bundled with WooCommerce will meet our minimum requirements. $error_message .= ' ' . __( 'There is a newer version of WooCommerce Admin bundled with WooCommerce.', 'woocommerce-payments' ); if ( current_user_can( 'deactivate_plugins' ) ) { $deactivate_url = wp_nonce_url( admin_url( 'plugins.php?action=deactivate&plugin=woocommerce-admin/woocommerce-admin.php' ), 'deactivate-plugin_woocommerce-admin/woocommerce-admin.php' ); $error_message .= ' <a href="' . $deactivate_url . '">' . __( 'Use the bundled version of WooCommerce Admin', 'woocommerce-payments' ) . '</a>'; } break; case self::WP_INCOMPATIBLE: $error_message = WC_Payments_Utils::esc_interpolated_html( sprintf( /* translators: %1: WooPayments, %2: required WP version number, %3: currently installed WP version number */ __( '%1$s requires <strong>WordPress %2$s</strong> or greater (you are using %3$s).', 'woocommerce-payments' ), 'WooPayments', $wp_version, get_bloginfo( 'version' ) ), [ 'strong' => '<strong>' ] ); if ( current_user_can( 'update_core' ) ) { $error_message .= ' <a href="' . admin_url( 'update-core.php' ) . '">' . __( 'Update WordPress', 'woocommerce-payments' ) . '</a>'; } break; case self::DEV_ASSETS_NOT_BUILT: $error_message = WC_Payments_Utils::esc_interpolated_html( sprintf( /* translators: %s: WooPayments */ __( 'You have installed a development version of %s which requires files to be built. From the plugin directory, run <code>npm run build:client</code> to build and minify assets. Alternatively, you can download a pre-built version of the plugin from the <a1>WordPress.org repository</a1> or by visiting the <a2>releases page in the GitHub repository</a2>.', 'woocommerce-payments' ), 'WooPayments' ), [ 'code' => '<code>', 'a1' => '<a href="https://wordpress.org/plugins/woocommerce-payments/">', 'a2' => '<a href="https://github.com/automattic/woocommerce-payments/releases/">', ] ); break; } return $error_message; } /** * Checks if current page is plugin installation process page. * * @return bool True when installing plugin. */ private static function is_at_plugin_install_page() { $cur_screen = get_current_screen(); return $cur_screen && 'update' === $cur_screen->id && 'plugins' === $cur_screen->parent_base; } /** * Check if the current WCPay Account has cache data. * * @return bool True if the cache data exists in wp_options. */ private static function has_cached_account_connection(): bool { $account_data = get_option( Database_Cache::ACCOUNT_KEY ); return isset( $account_data['data'] ) && is_array( $account_data['data'] ) && ! empty( $account_data['data'] ); } } includes/class-wc-payments-localization-service.php 0000777 00000010363 15251156605 0016601 0 ustar 00 <?php /** * WooCommerce Payments WC_Payments_Localization_Service Class * * @package WooCommerce\Payments */ use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface; defined( 'ABSPATH' ) || exit; /** * WC_Payments_Localization_Service. */ class WC_Payments_Localization_Service implements MultiCurrencyLocalizationInterface { const WCPAY_CURRENCY_FORMAT_TRANSIENT = 'wcpay_currency_format'; const WCPAY_LOCALE_INFO_TRANSIENT = 'wcpay_locale_info'; /** * Currency formatting map. * * @var array */ protected $currency_format = []; /** * Currency locale info. * * @var array */ protected $locale_info = []; /** * Constructor. */ public function __construct() { $this->load_locale_data(); } /** * Retrieves the currency's format from mapped data. * * @param string $currency_code The currency code. * * @return array The currency's format. */ public function get_currency_format( $currency_code ): array { // Default to USD settings if mapping not found. $currency_format = [ 'currency_pos' => 'left', 'thousand_sep' => ',', 'decimal_sep' => '.', 'num_decimals' => 2, ]; $locale = $this->get_user_locale(); $currency_options = $this->currency_format[ $currency_code ] ?? null; if ( $currency_options ) { // If there's no locale-specific formatting, default to the 'default' entry in the array. $currency_format = $currency_options[ $locale ] ?? $currency_options['default'] ?? $currency_format; } /** * Filter to edit formatting for a specific currency (wcpay_{currency_code}_format). * * This filter can be used to override the currency format for a specific currency. * The currency code in the filter name should be used in lowercase. * * @since 2.8.0 * * @param array $currency_format The currency format settings. * @param string $locale The user's locale. */ return apply_filters( 'wcpay_' . strtolower( $currency_code ) . '_format', $currency_format, $locale ); } /** * Returns the user locale. * * @return string The locale. */ public function get_user_locale(): string { return get_user_locale(); } // TODO: Add tests. /** * Returns the locale data for a country. * * @param string $country Country code. * * @return array Array with the country's locale data. Empty array if country not found. */ public function get_country_locale_data( $country ): array { return $this->locale_info[ $country ] ?? []; } /** * Loads locale data from WooCommerce core (/i18n/locale-info.php) and maps it * to be used by currency. * * @return void */ private function load_locale_data() { $transient_currency_format_data = get_transient( self::WCPAY_CURRENCY_FORMAT_TRANSIENT ); $transient_locale_info_data = get_transient( self::WCPAY_LOCALE_INFO_TRANSIENT ); if ( $transient_currency_format_data && $transient_locale_info_data ) { $this->currency_format = $transient_currency_format_data; $this->locale_info = $transient_locale_info_data; return; } $locale_info_path = WC()->plugin_path() . '/i18n/locale-info.php'; // The full locale data was introduced in the currency-info.php file. // If it doesn't exist we have to use the fallback. if ( ! file_exists( WC()->plugin_path() . '/i18n/currency-info.php' ) ) { $locale_info_path = WCPAY_ABSPATH . 'i18n/locale-info.php'; } $this->locale_info = include $locale_info_path; if ( is_array( $this->locale_info ) && 0 < count( $this->locale_info ) ) { // Extract the currency formatting options from the locale info. foreach ( $this->locale_info as $country_data ) { $currency_code = $country_data['currency_code']; foreach ( $country_data['locales'] as $locale => $locale_data ) { if ( empty( $locale_data ) ) { continue; } $this->currency_format[ $currency_code ][ $locale ] = [ 'currency_pos' => $locale_data['currency_pos'], 'thousand_sep' => $locale_data['thousand_sep'], 'decimal_sep' => $locale_data['decimal_sep'], 'num_decimals' => $country_data['num_decimals'], ]; } } set_transient( self::WCPAY_CURRENCY_FORMAT_TRANSIENT, $this->currency_format, DAY_IN_SECONDS ); set_transient( self::WCPAY_LOCALE_INFO_TRANSIENT, $this->locale_info, DAY_IN_SECONDS ); } } } includes/class-wc-payments-woopay-direct-checkout.php 0000777 00000012302 15251156605 0017037 0 ustar 00 <?php /** * Class WC_Payments_WooPay_Direct_Checkout * Adds support for WooPay direct checkout feature. * * @package WooCommerce\Payments */ use WCPay\WooPay\WooPay_Utilities; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class WC_Payments_WooPay_Direct_Checkout. */ class WC_Payments_WooPay_Direct_Checkout { /** * WooPay_Utilities instance. * * @var WooPay_Utilities */ private $woopay_utilities; /** * Initialize class actions. * * @param WooPay_Utilities $woopay_utilities WooPay utilities. */ public function __construct( WooPay_Utilities $woopay_utilities ) { $this->woopay_utilities = $woopay_utilities; } /** * Initialize the hooks. * * @return void */ public function init() { add_action( 'wp_footer', [ $this, 'scripts' ] ); add_filter( 'woocommerce_create_order', [ $this, 'maybe_use_store_api_draft_order_id' ] ); } /** * This filter is used to ensure the session's store_api_draft_order is used, if it exists. * This prevents a bug where the store_api_draft_order is not used and instead, a new * order_awaiting_payment is created during the checkout request. The bug being evident * if a product had one remaining stock and the store_api_draft_order was reserving it, * an order would fail to be placed since when order_awaiting_payment is created, it would * not be able to reserve the one stock. * * @param int $order_id The order ID being used. * @return int|mixed The new order ID to use. */ public function maybe_use_store_api_draft_order_id( $order_id ) { // Only apply this filter during the checkout request. $is_checkout = defined( 'WOOCOMMERCE_CHECKOUT' ) && WOOCOMMERCE_CHECKOUT; // Only apply this filter if the order ID is not already defined. $is_already_defined_order_id = ! empty( $order_id ); // Only apply this filter if the session doesn't already have an order_awaiting_payment. $is_order_awaiting_payment = isset( WC()->session->order_awaiting_payment ); // Only apply this filter if draft order ID exists. $has_draft_order = ! empty( WC()->session->get( 'store_api_draft_order' ) ); if ( ! $is_checkout || $is_already_defined_order_id || $is_order_awaiting_payment || ! $has_draft_order ) { return $order_id; } $draft_order_id = absint( WC()->session->get( 'store_api_draft_order' ) ); // Set the order status to "pending" payment, so that it can be resumed. $draft_order = wc_get_order( $draft_order_id ); $draft_order->set_status( 'pending' ); $draft_order->save(); // Move $draft_order_id in session, from store_api_draft_order to order_awaiting_payment. WC()->session->set( 'store_api_draft_order', null ); WC()->session->set( 'order_awaiting_payment', $draft_order_id ); return $order_id; } /** * Enqueue scripts. * * @return void */ public function scripts() { if ( ! $this->should_enqueue_scripts() ) { return; } if ( ! $this->woopay_utilities->should_enable_woopay_on_guest_checkout() ) { return; } // Enqueue the WCPay common config script only if it hasn't been enqueued yet. // This may happen when Direct Checkout is being enqueued on pages that are not the cart page, // such as the home and shop pages. if ( ! $this->is_cart_page() && ! $this->is_checkout_page() && WC_Payments_Features::is_woopay_direct_checkout_enabled() ) { WC_Payments::enqueue_woopay_common_config_script(); } WC_Payments::register_script_with_dependencies( 'WCPAY_WOOPAY_DIRECT_CHECKOUT', 'dist/woopay-direct-checkout' ); $direct_checkout_settings = [ 'params' => [ 'is_product_page' => $this->is_product_page(), ], ]; wp_localize_script( 'WCPAY_WOOPAY_DIRECT_CHECKOUT', 'wcpayWooPayDirectCheckout', $direct_checkout_settings ); wp_enqueue_script( 'WCPAY_WOOPAY_DIRECT_CHECKOUT' ); } /** * Check if the direct checkout scripts should be enqueued on the page. * * Scripts should be enqueued if: * - The current page is the cart page. * - The current page has a cart block. * - The current page has the blocks mini cart widget, i.e 'woocommerce_blocks_cart_enqueue_data' has been fired. * - The current page has the cart fragments script enqueued. which is enqueued by the shortcode mini cart widget. * * @return bool True if the scripts should be enqueued, false otherwise. */ private function should_enqueue_scripts(): bool { return $this->is_cart_page() || did_action( 'woocommerce_blocks_cart_enqueue_data' ) > 0 || ( wp_script_is( 'wc-cart-fragments', 'enqueued' ) && ! $this->is_checkout_page() ); } /** * Check if the current page is the cart page. * * @return bool True if the current page is the cart page, false otherwise. */ private function is_cart_page(): bool { return is_cart() || has_block( 'woocommerce/cart' ); } /** * Check if the current page is the checkout page. * * @return bool True if the current page is the checkout page, false otherwise. */ private function is_checkout_page(): bool { return is_checkout() || has_block( 'woocommerce/checkout' ); } /** * Check if the current page is the product page. * * @return bool True if the current page is the product page, false otherwise. */ private function is_product_page() { return is_product() || wc_post_content_has_shortcode( 'product_page' ); } } includes/class-logger-context.php 0000777 00000001521 15251156605 0013143 0 ustar 00 <?php /** * Class LoggerContext * * @package WooCommerce\Payments */ namespace WCPay; use WCPay\Internal\LoggerContext as InternalLoggerContext; defined( 'ABSPATH' ) || exit; // block direct access. /** * A wrapper class for accessing LoggerContext as a singletone. */ class Logger_Context { /** * Sets a context value. * * @param string $key The key to set. * @param string|int|float|bool|null $value The value to set. Null removes value. * * @return void */ public static function set_value( $key, $value ) { wcpay_get_container()->get( InternalLoggerContext::class )->set_value( $key, $value ); } /** * Gets a context. * * @return array<string, string> */ public static function get_context() { return wcpay_get_container()->get( InternalLoggerContext::class )->get_context(); } } includes/compat/subscriptions/class-wc-payments-email-failed-renewal-authentication.php 0000777 00000016427 15251156605 0025635 0 ustar 00 <?php /** * Class WC_Payments_Email_Failed_Renewal_Authentication * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Failed Renewal Authentication Notification. * * @extends WC_Email */ class WC_Payments_Email_Failed_Renewal_Authentication extends WC_Email { /** * An instance of the email, which would normally be sent after a failed payment. * * @var WC_Email_Failed_Order */ public $original_email; /** * Constructor. * * @param WC_Email_Failed_Order[] $email_classes All existing instances of WooCommerce emails. */ public function __construct( $email_classes = [] ) { $this->id = 'failed_renewal_authentication'; $this->title = __( 'Failed subscription renewal SCA authentication', 'woocommerce-payments' ); $this->description = __( 'Sent to a customer when a renewal fails because the transaction requires an SCA verification. The email contains renewal order information and payment links.', 'woocommerce-payments' ); $this->customer_email = true; $this->template_html = 'failed-renewal-authentication.php'; $this->template_plain = 'plain/failed-renewal-authentication.php'; $this->template_base = __DIR__ . '/emails/'; // Triggers the email at the correct hook. add_action( 'woocommerce_woocommerce_payments_payment_requires_action', [ $this, 'trigger' ] ); if ( isset( $email_classes['WCS_Email_Customer_Renewal_Invoice'] ) ) { $this->original_email = $email_classes['WCS_Email_Customer_Renewal_Invoice']; } // We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor. parent::__construct(); } /** * Generates the HTML for the email while keeping the `template_base` in mind. * * @return string */ public function get_content_html() { ob_start(); wc_get_template( $this->template_html, [ 'order' => $this->object, 'email_heading' => $this->get_heading(), 'sent_to_admin' => false, 'plain_text' => false, 'authorization_url' => $this->get_authorization_url( $this->object ), 'email' => $this, ], '', $this->template_base ); return ob_get_clean(); } /** * Generates the plain text for the email while keeping the `template_base` in mind. * * @return string */ public function get_content_plain() { ob_start(); wc_get_template( $this->template_plain, [ 'order' => $this->object, 'email_heading' => $this->get_heading(), 'sent_to_admin' => false, 'plain_text' => true, 'authorization_url' => $this->get_authorization_url( $this->object ), 'email' => $this, ], '', $this->template_base ); return ob_get_clean(); } /** * Generates the URL, which will be used to authenticate the payment. * * @param WC_Order $order The order whose payment needs authentication. * @return string */ public function get_authorization_url( $order ) { return add_query_arg( 'wcpay-confirmation', 1, $order->get_checkout_payment_url( false ) ); // nosemgrep: audit.php.wp.security.xss.query-arg -- server generated url is passed in. } /** * Uses specific fields from `WC_Email_Customer_Invoice` for this email. */ public function init_form_fields() { parent::init_form_fields(); $base_fields = $this->form_fields; $this->form_fields = [ 'enabled' => [ 'title' => _x( 'Enable/disable', 'an email notification', 'woocommerce-payments' ), 'type' => 'checkbox', 'label' => __( 'Enable this email notification', 'woocommerce-payments' ), 'default' => 'yes', ], 'subject' => $base_fields['subject'], 'heading' => $base_fields['heading'], 'email_type' => $base_fields['email_type'], ]; } /** * Returns the default subject of the email (modifiable in settings). * * @return string */ public function get_default_subject() { return __( 'Payment authorization needed for renewal of {site_title} order {order_number}', 'woocommerce-payments' ); } /** * Returns the default heading of the email (modifiable in settings). * * @return string */ public function get_default_heading() { return __( 'Payment authorization needed for renewal of order {order_number}', 'woocommerce-payments' ); } /** * Triggers the email while also disconnecting the original Subscriptions email. * * @param WC_Order $order The order that is being paid. */ public function trigger( $order ) { if ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order->get_id() ) || wcs_is_subscription( $order->get_id() ) || wcs_order_contains_renewal( $order->get_id() ) ) ) { if ( ! $this->is_enabled() ) { return; } $this->object = $order; $this->recipient = $order->get_billing_email(); $this->find['order_date'] = '{order_date}'; $this->replace['order_date'] = wc_format_datetime( $order->get_date_created() ); $this->find['order_number'] = '{order_number}'; $this->replace['order_number'] = $order->get_order_number(); $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); // Prevent the renewal email from WooCommerce Subscriptions from being sent. if ( isset( $this->original_email ) ) { remove_action( 'woocommerce_generated_manual_renewal_order_renewal_notification', [ $this->original_email, 'trigger', ] ); remove_action( 'woocommerce_order_status_failed_renewal_notification', [ $this->original_email, 'trigger', ] ); } // Prevent the retry email from WooCommerce Subscriptions from being sent. add_filter( 'wcs_get_retry_rule_raw', [ $this, 'prevent_retry_notification_email' ], 100, 3 ); // Send email to store owner indicating communication is happening with the customer to request authentication. add_filter( 'wcs_get_retry_rule_raw', [ $this, 'set_store_owner_custom_email' ], 100, 3 ); } } /** * Prevent all customer-facing retry notifications from being sent after this email. * * @param array $rule_array The raw details about the retry rule. * @param int $retry_number The number of the retry. * @param int $order_id The ID of the order that needs payment. * * @return array */ public function prevent_retry_notification_email( $rule_array, $retry_number, $order_id ) { if ( wcs_get_objects_property( $this->object, 'id' ) === $order_id ) { $rule_array['email_template_customer'] = ''; } return $rule_array; } /** * Send store owner a different email when the retry is related to an authentication required error. * * @param array $rule_array The raw details about the retry rule. * @param int $retry_number The number of the retry. * @param int $order_id The ID of the order that needs payment. * * @return array */ public function set_store_owner_custom_email( $rule_array, $retry_number, $order_id ) { if ( wcs_get_objects_property( $this->object, 'id' ) === $order_id && '' !== $rule_array['email_template_admin'] // Only send our email if a retry admin email was already going to be sent. ) { $rule_array['email_template_admin'] = 'WC_Payments_Email_Failed_Authentication_Retry'; } return $rule_array; } } includes/compat/subscriptions/trait-wc-payments-subscriptions-utilities.php 0000777 00000012314 15251156605 0023601 0 ustar 00 <?php /** * Trait WC_Payments_Subscriptions_Utilities * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Utility functions related to WC Subscriptions. */ trait WC_Payments_Subscriptions_Utilities { /** * Checks if subscriptions are enabled on the site. * * Subscriptions functionality is enabled if the WC Subscriptions plugin is active and greater than v 2.2, or the base feature is turned on. * * @return bool Whether subscriptions is enabled or not. */ public function is_subscriptions_enabled() { if ( $this->is_subscriptions_plugin_active() ) { return version_compare( $this->get_subscriptions_plugin_version(), '2.2.0', '>=' ); } // TODO update this once we know how the base library feature will be enabled. return class_exists( 'WC_Subscriptions_Core_Plugin' ); } /** * Returns whether this user is changing the payment method for a subscription. * * @return bool */ public function is_changing_payment_method_for_subscription() { if ( isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return wcs_is_subscription( wc_clean( wp_unslash( $_GET['change_payment_method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification } return false; } /** * Returns boolean value indicating whether payment for an order will be recurring, * as opposed to single. * * @param int $order_id ID for corresponding WC_Order in process. * * @return bool */ public function is_payment_recurring( $order_id ) { if ( ! $this->is_subscriptions_enabled() ) { return false; } return $this->is_changing_payment_method_for_subscription() || wcs_order_contains_subscription( $order_id ); } /** * Returns a boolean value indicating whether the save payment checkbox should be * displayed during checkout. * * Returns `false` if the cart currently has a subscriptions or if the request has a * `change_payment_method` GET parameter. Returns the value in `$display` otherwise. * * @param bool $display Bool indicating whether to show the save payment checkbox in the absence of subscriptions. * * @return bool Indicates whether the save payment method checkbox should be displayed or not. */ public function display_save_payment_method_checkbox( $display ) { if ( ! class_exists( 'WC_Subscriptions_Cart' ) ) { return false; } if ( WC_Subscriptions_Cart::cart_contains_subscription() || $this->is_changing_payment_method_for_subscription() ) { return false; } // Only render the "Save payment method" checkbox if there are no subscription products in the cart. return $display; } /** * Returns boolean on whether current WC_Cart or WC_Subscriptions_Cart * contains a subscription or subscription renewal item * * @return bool */ public function is_subscription_item_in_cart() { if ( class_exists( 'WC_Subscriptions_Cart' ) && $this->is_subscriptions_enabled() ) { return WC_Subscriptions_Cart::cart_contains_subscription() || $this->cart_contains_renewal(); } return false; } /** * Checks the cart to see if it contains a subscription product renewal. * * @return mixed The cart item containing the renewal as an array, else false. */ public function cart_contains_renewal() { if ( ! function_exists( 'wcs_cart_contains_renewal' ) ) { return false; } return wcs_cart_contains_renewal(); } /** * Checks if the WC Subscriptions plugin is active. * * @return bool Whether the plugin is active or not. */ public function is_subscriptions_plugin_active() { return class_exists( 'WC_Subscriptions' ); } /** * Gets the version of WooCommerce Subscriptions that is active. * * @return null|string The plugin version. Returns null when WC Subscriptions is not active/loaded. */ public function get_subscriptions_plugin_version() { return class_exists( 'WC_Subscriptions' ) ? WC_Subscriptions::$version : null; } /** * Gets the version of the subscriptions-core library. * * @return null|string The version number of subscriptions-core or null if not active. */ public function get_subscriptions_core_version() { $subscriptions_core_instance = WC_Subscriptions_Core_Plugin::instance(); // For backwards compatibility with older versions of WC Subscriptions, we need to do an existence check. if ( method_exists( $subscriptions_core_instance, 'get_library_version' ) ) { return $subscriptions_core_instance->get_library_version(); } return $subscriptions_core_instance ? $subscriptions_core_instance->get_plugin_version() : null; } /** * Gets the total number of subscriptions that have already been migrated. * * @return int The total number of subscriptions migrated. */ public function get_subscription_migrated_count() { if ( ! function_exists( 'wcs_get_orders_with_meta_query' ) ) { return 0; } return count( wcs_get_orders_with_meta_query( [ 'status' => 'any', 'return' => 'ids', 'type' => 'shop_subscription', 'limit' => -1, 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query [ 'key' => '_migrated_wcpay_subscription_id', 'compare' => 'EXISTS', ], ], ] ) ); } } includes/compat/subscriptions/class-wc-payments-email-failed-authentication-retry.php 0000777 00000016447 15251156605 0025347 0 ustar 00 <?php /** * Admin email about payment retry failed due to authentication * * Email sent to admins when an attempt to automatically process a subscription renewal payment has failed * with the `authentication_needed` error, and a retry rule has been applied to retry the payment in the future. * * @extends WC_Email_Failed_Order * @package WooCommerce\Payments */ use WCPay\Logger; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class WC_Payments_Email_Failed_Authentication_Retry * * An email sent to the admin when payment fails to go through due to authentication_required error. * * @extends WC_Email_Failed_Order * * @filter woocommerce_email_preview_dummy_order * Filters the dummy order object used for email previews. * @param WC_Order|bool $order The order object or false. * @return WC_Order The filtered order object. * * @filter woocommerce_email_preview_dummy_retry * Filters the dummy retry object used for email previews. * @param WCS_Retry|bool $retry The retry object or false. * @return WCS_Retry|null The filtered retry object or null if WCS_Retry class doesn't exist. * * @filter woocommerce_email_preview_placeholders * Filters the email preview placeholders. * @param array $placeholders Array of email preview placeholders. * @return array Modified array of placeholders. */ class WC_Payments_Email_Failed_Authentication_Retry extends WC_Email_Failed_Order { /** * The details of the last retry (if any) recorded for a given order * * @var WCS_Retry */ private $retry; /** * Constructor */ public function __construct() { $this->id = 'failed_authentication_requested'; $this->title = __( 'Payment authentication requested email', 'woocommerce-payments' ); $this->description = __( 'Payment authentication requested emails are sent to chosen recipient(s) when an attempt to automatically process a subscription renewal payment fails because the transaction requires an SCA verification, the customer is requested to authenticate the payment, and a retry rule has been applied to notify the customer again within a certain time period.', 'woocommerce-payments' ); $this->heading = __( 'Automatic renewal payment failed due to authentication required', 'woocommerce-payments' ); $this->subject = __( '[{site_title}] Automatic payment failed for {order_number}. Customer asked to authenticate payment and will be notified again {retry_time}', 'woocommerce-payments' ); $this->template_html = 'failed-renewal-authentication-requested.php'; $this->template_plain = 'plain/failed-renewal-authentication-requested.php'; $this->template_base = __DIR__ . '/emails/'; $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); // We want all the parent's methods, with none of its properties, so call its parent's constructor, rather than my parent constructor. WC_Email::__construct(); // Add email preview filters. add_filter( 'woocommerce_email_preview_dummy_order', [ $this, 'get_preview_order' ], 10, 1 ); add_filter( 'woocommerce_email_preview_dummy_retry', [ $this, 'get_preview_retry' ], 10, 1 ); add_filter( 'woocommerce_email_preview_placeholders', [ $this, 'get_preview_placeholders' ], 10, 1 ); } /** * Get a dummy order for email preview. * * @param WC_Order|bool $order The order object or false. * @return WC_Order */ public function get_preview_order( $order ) { if ( ! $order instanceof WC_Order ) { $order = wc_create_order(); $order->set_status( 'failed' ); $order->set_billing_first_name( 'John' ); $order->set_billing_last_name( 'Doe' ); $order->set_billing_email( 'john.doe@example.com' ); $order->set_total( 99.99 ); $order->save(); } return $order; } /** * Get a dummy retry object for email preview. * * @param WCS_Retry|bool $retry The retry object or false. * @return WCS_Retry|null */ public function get_preview_retry( $retry ) { if ( ! class_exists( 'WCS_Retry' ) || ! function_exists( 'wcs_get_human_time_diff' ) ) { return null; } if ( ! $retry instanceof WCS_Retry ) { $retry_data = [ 'time' => time() + DAY_IN_SECONDS, 'order_id' => 0, 'retry_number' => 1, 'status' => 'pending', ]; $retry = new WCS_Retry( $retry_data ); } return $retry; } /** * Get preview placeholders. * * @param array $placeholders The placeholders array. * @return array */ public function get_preview_placeholders( $placeholders ) { $retry = $this->get_preview_retry( false ); $retry_time = ''; if ( $retry && function_exists( 'wcs_get_human_time_diff' ) ) { $retry_time = wcs_get_human_time_diff( $retry->get_time() ); } $placeholders['{retry_time}'] = $retry_time; return $placeholders; } /** * Get the default e-mail subject. * * @return string */ public function get_default_subject() { return $this->subject; } /** * Get the default e-mail heading. * * @return string */ public function get_default_heading() { return $this->heading; } /** * Trigger. * * @param int $order_id The order ID. * @param WC_Order|null $order Order object. */ public function trigger( $order_id, $order = null ) { if ( ! $this->is_enabled() || ! $this->get_recipient() ) { return; } $this->object = $order; if ( class_exists( 'WCS_Retry_Manager' ) && function_exists( 'wcs_get_human_time_diff' ) ) { $this->retry = WCS_Retry_Manager::store()->get_last_retry_for_order( wcs_get_objects_property( $order, 'id' ) ); } else { Logger::log( 'WCS_Retry_Manager class or does not exist. Not able to send admin email about customer notification for authentication required for renewal payment.' ); return; } // Set up order number replacement. $this->find['order-number'] = '{order_number}'; $this->replace['order-number'] = $this->object->get_order_number(); // Set up retry time replacement. $retry_time = ''; if ( $this->retry && function_exists( 'wcs_get_human_time_diff' ) ) { $retry_time = wcs_get_human_time_diff( $this->retry->get_time() ); } $this->placeholders['{retry_time}'] = $retry_time; $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); } /** * Get content html. * * @return string */ public function get_content_html() { // Ensure retry object is initialized for preview. if ( ! isset( $this->retry ) ) { $this->retry = $this->get_preview_retry( false ); } return wc_get_template_html( $this->template_html, [ 'order' => $this->object, 'retry' => $this->retry, 'email_heading' => $this->get_heading(), 'sent_to_admin' => true, 'plain_text' => false, 'email' => $this, ], '', $this->template_base ); } /** * Get content plain. * * @return string */ public function get_content_plain() { // Ensure retry object is initialized for preview. if ( ! isset( $this->retry ) ) { $this->retry = $this->get_preview_retry( false ); } return wc_get_template_html( $this->template_plain, [ 'order' => $this->object, 'retry' => $this->retry, 'email_heading' => $this->get_heading(), 'sent_to_admin' => true, 'plain_text' => true, 'email' => $this, ], '', $this->template_base ); } } includes/compat/subscriptions/emails/failed-renewal-authentication.php 0000777 00000002053 15251156605 0022440 0 ustar 00 <?php /** * Customer email about payment retry failed due to authentication. * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } ?> <?php do_action( 'woocommerce_email_header', $email_heading, $email ); ?> <p> <?php // translators: %1$s: name of the blog, %2$s: link to payment re-authentication URL, note: no full stop due to url at the end. echo wp_kses( sprintf( _x( 'The automatic payment to renew your subscription with %1$s has failed. To reactivate the subscription, please login and authorize the renewal from your account page: %2$s', 'In failed renewal authentication email', 'woocommerce-payments' ), esc_html( get_bloginfo( 'name' ) ), '<a href="' . esc_url( $authorization_url ) . '">' . esc_html__( 'Authorize the payment »', 'woocommerce-payments' ) . '</a>' ), [ 'a' => [ 'href' => true ] ] ); ?> </p> <?php do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email ); ?> <?php do_action( 'woocommerce_email_footer', $email ); ?> includes/compat/subscriptions/emails/failed-renewal-authentication-requested.php 0000777 00000002767 15251156605 0024453 0 ustar 00 <?php /** * Admin email about payment retry failed due to authentication. * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Output the email header. */ do_action( 'woocommerce_email_header', $email_heading, $email ); ?> <p> <?php echo esc_html( sprintf( // translators: %1$s: an order number, %2$s: the customer's full name, %3$s: lowercase human time diff in the form returned by wcs_get_human_time_diff(), e.g. 'in 12 hours'. _x( 'The automatic recurring payment for order %1$s from %2$s has failed. The customer was sent an email requesting authentication of payment. If the customer does not authenticate the payment, they will be requested by email again %3$s.', 'In admin renewal failed email', 'woocommerce-payments' ), $order->get_order_number(), $order->get_formatted_billing_full_name(), wcs_get_human_time_diff( $retry->get_time() ) ) ); ?> </p> <p><?php esc_html_e( 'The renewal order is as follows:', 'woocommerce-payments' ); ?></p> <?php /** * Shows the order details table. */ do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); /** * Shows order meta data. */ do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); /** * Shows customer details, and email address. */ do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); /** * Output the email footer. */ do_action( 'woocommerce_email_footer', $email ); includes/compat/subscriptions/emails/plain/failed-renewal-authentication.php 0000777 00000002140 15251156605 0023540 0 ustar 00 <?php /** * Customer email about payment retry failed due to authentication. * * @package WooCommerce\Payments */ // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } echo $email_heading . "\n\n"; // translators: %1$s: name of the blog, %2$s: link to checkout payment url, note: no full stop due to url at the end. printf( esc_html_x( 'The automatic payment to renew your subscription with %1$s has failed. To reactivate the subscription, please login and authorize the renewal from your account page: %2$s', 'In failed renewal authentication email', 'woocommerce-payments' ), esc_html( get_bloginfo( 'name' ) ), esc_attr( $authorization_url ) ); echo "\n\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n"; do_action( 'woocommerce_subscriptions_email_order_details', $order, $sent_to_admin, $plain_text, $email ); echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); includes/compat/subscriptions/emails/plain/failed-renewal-authentication-requested.php 0000777 00000003310 15251156605 0025537 0 ustar 00 <?php /** * Admin email about payment retry failed due to authentication. * * @package WooCommerce\Payments */ // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped if ( ! defined( 'ABSPATH' ) ) { exit; } echo '= ' . $email_heading . " =\n\n"; printf( // translators: %1$s: an order number, %2$s: the customer's full name, %3$s: lowercase human time diff in the form returned by wcs_get_human_time_diff(), e.g. 'in 12 hours'. _x( 'The automatic recurring payment for order %1$s from %2$s has failed. The customer was sent an email requesting authentication of payment. If the customer does not authenticate the payment, they will be requested by email again %3$s.', 'In admin renewal failed email', 'woocommerce-payments' ), $order->get_order_number(), $order->get_formatted_billing_full_name(), wcs_get_human_time_diff( $retry->get_time() ) ) . "\n\n"; printf( __( 'The renewal order is as follows:', 'woocommerce-payments' ) ) . "\n\n"; echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; /** * Shows the order details table. */ do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; /** * Shows order meta data. */ do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); /** * Shows customer details, and email address. */ do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n"; echo apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ); includes/compat/subscriptions/trait-wc-payment-gateway-wcpay-subscriptions.php 0000777 00000134111 15251156605 0024165 0 ustar 00 <?php /** * Trait WC_Payment_Gateway_WCPay_Subscriptions_Trait * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } use WCPay\Core\Server\Request\Get_Intention; use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\API_Merchant_Exception; use WCPay\Exceptions\Invalid_Payment_Method_Exception; use WCPay\Exceptions\Add_Payment_Method_Exception; use WCPay\Exceptions\Order_Not_Found_Exception; use WCPay\Logger; use WCPay\Payment_Information; use WCPay\Constants\Payment_Type; use WCPay\Constants\Payment_Initiated_By; use WCPay\Constants\Intent_Status; /** * Gateway class for WooPayments, with added compatibility with WooCommerce Subscriptions. */ trait WC_Payment_Gateway_WCPay_Subscriptions_Trait { use WC_Payments_Subscriptions_Utilities; /** * Retrieve payment token from a subscription or order. * * @param WC_Order $order Order or subscription object. * * @return null|WC_Payment_Token Last token associated with order or subscription. */ abstract protected function get_payment_token( $order ); /** * Process the payment for a given order. * * @param WC_Cart|null $cart Cart. * @param WCPay\Payment_Information $payment_information Payment info. * @param bool $scheduled_subscription_payment Used to determinate is scheduled subscription payment to add more fields into API request. * * @return array|null An array with result of payment and redirect URL, or nothing. * @throws API_Exception Error processing the payment. * @throws Add_Payment_Method_Exception When $0 order processing failed. */ abstract public function process_payment_for_order( $cart, $payment_information, $scheduled_subscription_payment = false ); /** * Get the payment method to use for the intent. * * @return string The payment method to use for the intent (e.g. 'card') */ abstract public function get_payment_method_to_use_for_intent(); /** * Saves the payment token to the order. * * @param WC_Order $order The order. * @param WC_Payment_Token $token The token to save. */ abstract public function add_token_to_order( $order, $token ); /** * Returns a formatted token list for a user. * * @param int $user_id The user ID. */ abstract protected function get_user_formatted_tokens_array( $user_id ); /** * Prepares the payment information object. * * @param WC_Order $order The order whose payment will be processed. * @return Payment_Information An object, which describes the payment. */ abstract protected function prepare_payment_information( $order ); /** * Stores the payment method meta table name * * @var string */ private static $payment_method_meta_table = 'wc_order_tokens'; /** * Stores the payment method meta key name * * @var string */ private static $payment_method_meta_key = 'token'; /** * Stores a flag to indicate if the subscription integration hooks have been attached. * * The callbacks attached as part of maybe_init_subscriptions() only need to be attached once to avoid duplication. * * @var bool False by default, true once the callbacks have been attached. */ private static $has_attached_integration_hooks = false; /** * Used to temporary keep the state of the order_pay value on the Pay for order page with the SCA authorization flow. * For more details, see remove_order_pay_var and restore_order_pay_var hooks. * * @var string|int */ private $order_pay_var; /** * Initialize subscription support and hooks. */ public function maybe_init_subscriptions() { if ( ! $this->is_subscriptions_enabled() ) { return; } /* * Base set of subscription features to add. * The WCPay payment gateway supports these features * for both WCPay Subscriptions and WooCommerce Subscriptions. */ $payment_gateway_features = [ 'multiple_subscriptions', 'subscription_cancellation', 'subscription_payment_method_change_admin', 'subscription_payment_method_change_customer', 'subscription_payment_method_change', 'subscription_reactivation', 'subscription_suspension', 'subscriptions', ]; if ( ! WC_Payments_Features::should_use_stripe_billing() ) { /* * Subscription amount & date changes are only supported * when Stripe Billing is not in use. */ $payment_gateway_features = array_merge( $payment_gateway_features, [ 'subscription_amount_changes', 'subscription_date_changes', ] ); } else { /* * The gateway_scheduled_payments feature is only supported * for WCPay Subscriptions. */ $payment_gateway_features[] = 'gateway_scheduled_payments'; } $this->supports = array_merge( $this->supports, $payment_gateway_features ); } /** * Initializes this trait's WP hooks. * * The hooks are not initialized more than once or if the ID of the attached gateway is not 'woocommerce_payments'. * * @return void */ public function maybe_init_subscriptions_hooks() { /** * The following callbacks are only attached once to avoid duplication. * The callbacks are also only intended to be attached for the WCPay core payment gateway ($this->id = 'woocommerce_payments'). * * If new payment method IDs (eg 'sepa_debit') are added to this condition in the future, care should be taken to ensure duplication, * including double renewal charging, isn't introduced. */ if ( self::$has_attached_integration_hooks || 'woocommerce_payments' !== $this->id || ! $this->is_subscriptions_enabled() ) { return; } self::$has_attached_integration_hooks = true; add_filter( 'woocommerce_email_classes', [ $this, 'add_emails' ], 20 ); add_filter( 'woocommerce_available_payment_gateways', [ $this, 'prepare_order_pay_page' ] ); add_action( 'woocommerce_checkout_subscription_created', [ $this, 'maybe_force_subscription_to_manual' ], 10, 1 ); add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, [ $this, 'scheduled_subscription_payment' ], 10, 2 ); add_action( 'woocommerce_subscription_failing_payment_method_updated_' . $this->id, [ $this, 'update_failing_payment_method' ], 10, 2 ); add_filter( 'wc_payments_display_save_payment_method_checkbox', [ $this, 'display_save_payment_method_checkbox' ], 10 ); // Display the credit card used for a subscription in the "My Subscriptions" table. add_filter( 'woocommerce_my_subscriptions_payment_method', [ $this, 'maybe_render_subscription_payment_method' ], 10, 2 ); // Hide "Change payment" button for manual subscriptions with non-reusable payment methods. add_filter( 'wcs_view_subscription_actions', [ $this, 'maybe_hide_change_payment_for_manual_subscriptions' ], 10, 2 ); // Hide "Auto-renew" toggle for manual subscriptions with non-reusable payment methods. add_filter( 'user_has_cap', [ $this, 'maybe_hide_auto_renew_toggle_for_manual_subscriptions' ], 100, 3 ); // Used to filter out unwanted metadata on new renewal orders. if ( ! class_exists( 'WC_Subscriptions_Data_Copier' ) ) { add_filter( 'wcs_renewal_order_meta_query', [ $this, 'update_renewal_meta_data' ], 10, 3 ); } else { add_filter( 'wc_subscriptions_renewal_order_data', [ $this, 'remove_data_renewal_order' ], 10, 3 ); } // Allow store managers to manually set Stripe as the payment method on a subscription. add_filter( 'woocommerce_subscription_payment_meta', [ $this, 'add_subscription_payment_meta' ], 10, 2 ); add_filter( 'woocommerce_subscription_validate_payment_meta', [ $this, 'validate_subscription_payment_meta' ], 10, 3 ); add_action( 'wcs_save_other_payment_meta', [ $this, 'save_meta_in_order_tokens' ], 10, 4 ); // To make sure payment meta is copied from subscription to order. add_filter( 'wcs_copy_payment_meta_to_order', [ $this, 'append_payment_meta' ], 10, 3 ); add_filter( 'woocommerce_subscription_note_old_payment_method_title', [ $this, 'get_specific_old_payment_method_title' ], 10, 3 ); add_filter( 'woocommerce_subscription_note_new_payment_method_title', [ $this, 'get_specific_new_payment_method_title' ], 10, 3 ); add_action( 'woocommerce_admin_order_data_after_billing_address', [ $this, 'add_payment_method_select_to_subscription_edit' ] ); /* * WC subscriptions hooks into the "template_redirect" hook with priority 100. * If the screen is "Pay for order" and the order is a subscription renewal, it redirects to the plain checkout. * See: https://github.com/woocommerce/woocommerce-subscriptions/blob/99a75687e109b64cbc07af6e5518458a6305f366/includes/class-wcs-cart-renewal.php#L165 * If we are in the "You just need to authorize SCA" flow, we don't want that redirection to happen. */ add_action( 'template_redirect', [ $this, 'remove_order_pay_var' ], 99 ); add_action( 'template_redirect', [ $this, 'restore_order_pay_var' ], 101 ); // Update subscriptions token when user sets a default payment method. add_filter( 'woocommerce_subscriptions_update_subscription_token', [ $this, 'update_subscription_token' ], 10, 3 ); add_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', [ $this, 'update_payment_method_for_subscriptions' ], 10, 3 ); // AJAX handler for fetching payment tokens when customer changes. add_action( 'wp_ajax_wcpay_get_user_payment_tokens', [ $this, 'ajax_get_user_payment_tokens' ] ); } /** * Stops WC Subscriptions from updating the payment method for subscriptions. * * @param bool $update_payment_method Whether to update the payment method. * @param string $new_payment_method The new payment method. * @param WC_Subscription $subscription The subscription. * @return bool */ public function update_payment_method_for_subscriptions( $update_payment_method, $new_payment_method, $subscription ) { // Skip if the change payment method request was not made yet. if ( ! isset( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_wcsnonce'] ), 'wcs_change_payment_method' ) ) { return $update_payment_method; } // Avoid interfering with use-cases not related to updating payment method for subscriptions. if ( ! $this->is_changing_payment_method_for_subscription() ) { return $update_payment_method; } // Avoid interfering with other payment gateways' operations. if ( $new_payment_method !== $this->id ) { return $update_payment_method; } // If the payment method is a saved payment method, we don't need to stop WC Subscriptions from updating it. if ( ( isset( $_POST[ 'wc-' . $new_payment_method . '-payment-token' ] ) && 'new' !== $_POST[ 'wc-' . $new_payment_method . '-payment-token' ] ) ) { return $update_payment_method; } return false; } /** * Adds the necessary hooks to modify the "Pay for order" page in order to clean * it up and prepare it for the PaymentIntents modal to confirm a payment. * * @param WC_Payment_Gateway[] $gateways A list of all available gateways. * @return WC_Payment_Gateway[] Either the same list or an empty one in the right conditions. */ public function prepare_order_pay_page( $gateways ) { if ( ! is_wc_endpoint_url( 'order-pay' ) || ! isset( $_GET['wcpay-confirmation'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return $gateways; } try { if ( ! $this->prepare_intent_for_order_pay_page() ) { return $gateways; } } catch ( Exception $e ) { // Just show the full order pay page if there was a problem preparing the Payment Intent. return $gateways; } add_filter( 'woocommerce_checkout_show_terms', '__return_false' ); add_filter( 'woocommerce_pay_order_button_html', '__return_false' ); add_filter( 'woocommerce_available_payment_gateways', '__return_empty_array' ); add_filter( 'woocommerce_no_available_payment_methods_message', [ $this, 'change_no_available_methods_message' ] ); return []; } /** * Prepares the Payment Intent for it to be completed in the "Pay for Order" page. * * @return bool True if the Intent was fetched and prepared successfully, false otherwise. */ public function prepare_intent_for_order_pay_page(): bool { $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) ); $request = Get_Intention::create( $order->get_transaction_id() ); $request->set_hook_args( $order ); $intent = $request->send(); if ( ! $intent || Intent_Status::REQUIRES_ACTION !== $intent->get_status() ) { return false; } $js_config = WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config(); $js_config['intentSecret'] = $intent->get_client_secret(); $js_config['updateOrderNonce'] = wp_create_nonce( 'wcpay_update_order_status_nonce' ); wp_localize_script( 'WCPAY_CHECKOUT', 'wcpayConfig', $js_config ); wp_enqueue_script( 'WCPAY_CHECKOUT' ); return true; } /** * Changes the text of the "No available methods" message to one that indicates * the need for a PaymentIntent to be confirmed. * * @return string the new message. */ public function change_no_available_methods_message() { return wpautop( __( "Almost there!\n\nYour order has already been created, the only thing that still needs to be done is for you to authorize the payment with your bank.", 'woocommerce-payments' ) ); } /** * Prepares the payment information object. * * @param Payment_Information $payment_information The payment information from parent gateway. * @param int $order_id The order ID whose payment will be processed. * @return Payment_Information An object, which describes the payment. */ protected function maybe_prepare_subscription_payment_information( $payment_information, $order_id ) { if ( ! $this->is_payment_recurring( $order_id ) ) { return $payment_information; } // Subs-specific behavior starts here. $payment_information->set_payment_type( Payment_Type::RECURRING() ); // The payment method is always saved for subscriptions. $payment_information->must_save_payment_method_to_store(); $payment_information->set_is_changing_payment_method_for_subscription( $this->is_changing_payment_method_for_subscription() ); return $payment_information; } /** * Process a scheduled subscription payment. * * @param float $amount The amount to charge. * @param WC_Order $renewal_order A WC_Order object created to record the renewal payment. */ public function scheduled_subscription_payment( $amount, $renewal_order ) { // Exit early if the order belongs to a WCPay Subscription. The payment will be processed by the subscription via webhooks. if ( $this->is_wcpay_subscription_renewal_order( $renewal_order ) ) { return; } $token = $this->get_payment_token( $renewal_order ); if ( is_null( $token ) && ! WC_Payments::is_network_saved_cards_enabled() ) { $renewal_order->add_order_note( 'Subscription renewal failed: No saved payment method found.' ); Logger::error( 'There is no saved payment token for order #' . $renewal_order->get_id() ); // TODO: Update to use Order_Service->mark_payment_failed. $renewal_order->update_status( 'failed' ); return; } $customer_id = $this->order_service->get_customer_id_for_order( $renewal_order ); try { $payment_information = new Payment_Information( '', $renewal_order, Payment_Type::RECURRING(), $token, Payment_Initiated_By::MERCHANT(), null, null, '', $this->get_payment_method_to_use_for_intent(), $customer_id ); $this->process_payment_for_order( null, $payment_information, true ); } catch ( API_Exception $e ) { Logger::error( 'Error processing subscription renewal: ' . $e->getMessage() ); // TODO: Update to use Order_Service->mark_payment_failed. $renewal_order->update_status( 'failed' ); if ( ! empty( $payment_information ) ) { $error_details = esc_html( rtrim( $e->getMessage(), '.' ) ); if ( $e instanceof API_Merchant_Exception ) { $error_details = $error_details . '. ' . esc_html( rtrim( $e->get_merchant_message(), '.' ) ); } $note = sprintf( WC_Payments_Utils::esc_interpolated_html( /* translators: %1: the failed payment amount, %2: error message */ __( 'A payment of %1$s <strong>failed</strong> to complete with the following message: <code>%2$s</code>.', 'woocommerce-payments' ), [ 'strong' => '<strong>', 'code' => '<code>', ] ), WC_Payments_Explicit_Price_Formatter::get_explicit_price( wc_price( $amount, [ 'currency' => WC_Payments_Utils::get_order_intent_currency( $renewal_order ) ] ), $renewal_order ), $error_details ); $renewal_order->add_order_note( $note ); } } } /** * Adds the payment token from a failed renewal order to the provided subscription. * * @param WC_Subscription $subscription The subscription to be updated. * @param WC_Order $renewal_order The failed renewal order. */ public function update_failing_payment_method( $subscription, $renewal_order ) { $renewal_token = $this->get_payment_token( $renewal_order ); if ( is_null( $renewal_token ) ) { $renewal_order->add_order_note( 'Unable to update subscription payment method: No valid payment token or method found.' ); Logger::error( 'Failing subscription could not be updated: there is no saved payment token for order #' . $renewal_order->get_id() ); return; } $this->add_token_to_order( $subscription, $renewal_token ); } /** * Return the payment meta data for this payment gateway. * * @param WC_Subscription $subscription The subscription order. * @return array */ private function get_payment_meta( $subscription ) { $active_token = $this->get_payment_token( $subscription ); return [ self::$payment_method_meta_table => [ self::$payment_method_meta_key => [ 'label' => __( 'Saved payment method', 'woocommerce-payments' ), 'value' => empty( $active_token ) ? '' : (string) $active_token->get_id(), ], ], ]; } /** * Append payment meta if order and subscription are using WCPay as payment method and if passed payment meta is an array. * * @param array $payment_meta Associative array of meta data required for automatic payments. * @param WC_Order $order The subscription's related order. * @param WC_Subscription $subscription The subscription order. * @return array */ public function append_payment_meta( $payment_meta, $order, $subscription ) { if ( $this->id !== $order->get_payment_method() || $this->id !== $subscription->get_payment_method() ) { return $payment_meta; } if ( ! is_array( $payment_meta ) ) { return $payment_meta; } return array_merge( $payment_meta, $this->get_payment_meta( $subscription ) ); } /** * Include the payment meta data required to process automatic recurring payments so that store managers can * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+. * * @param array $payment_meta Associative array of meta data required for automatic payments. * @param WC_Subscription $subscription The subscription order. * @return array */ public function add_subscription_payment_meta( $payment_meta, $subscription ) { $payment_meta[ $this->id ] = $this->get_payment_meta( $subscription ); // Display select element on newer Subscriptions versions. add_action( sprintf( 'woocommerce_subscription_payment_meta_input_%s_%s_%s', WC_Payment_Gateway_WCPay::GATEWAY_ID, self::$payment_method_meta_table, self::$payment_method_meta_key ), [ $this, 'render_custom_payment_meta_input' ], 10, 3 ); return $payment_meta; } /** * Validate the payment meta data required to process automatic recurring payments so that store managers can * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+. * * @param string $payment_gateway_id The ID of the payment gateway to validate. * @param array $payment_meta Associative array of meta data required for automatic payments. * @param WC_Subscription $subscription The subscription order. * * @throws Invalid_Payment_Method_Exception When $payment_meta is not valid. */ public function validate_subscription_payment_meta( $payment_gateway_id, $payment_meta, $subscription ) { if ( $this->id !== $payment_gateway_id ) { return; } if ( empty( $payment_meta[ self::$payment_method_meta_table ][ self::$payment_method_meta_key ]['value'] ) ) { throw new Invalid_Payment_Method_Exception( __( 'A customer saved payment method was not selected for this order.', 'woocommerce-payments' ), 'payment_method_not_selected' ); } $token = WC_Payment_Tokens::get( $payment_meta[ self::$payment_method_meta_table ][ self::$payment_method_meta_key ]['value'] ); if ( empty( $token ) ) { throw new Invalid_Payment_Method_Exception( __( 'The saved payment method selected is invalid or does not exist.', 'woocommerce-payments' ), 'payment_method_token_not_found' ); } if ( $subscription->get_user_id() !== $token->get_user_id() ) { throw new Invalid_Payment_Method_Exception( __( 'The saved payment method selected does not belong to this order\'s customer.', 'woocommerce-payments' ), 'payment_method_token_not_owned' ); } } /** * Saves the payment token to the order. * * @param WC_Order $order The order. * @param WC_Payment_Token $token The token to save. */ public function maybe_add_token_to_subscription_order( $order, $token ) { if ( $this->is_subscriptions_enabled() ) { $subscriptions = wcs_get_subscriptions_for_order( $order->get_id() ); if ( is_array( $subscriptions ) ) { foreach ( $subscriptions as $subscription ) { $payment_token = $this->get_payment_token( $subscription ); if ( is_null( $payment_token ) || $token->get_id() !== $payment_token->get_id() ) { $subscription->add_payment_token( $token ); } } } } } /** * Save subscriptions payment_method metadata to the order tokens when its type is wc_order_tokens. * * @param WC_Subscription $subscription The subscription to be updated. * @param string $table Where to store and retrieve the metadata. * @param string $meta_key Meta key to be updated. * @param string $meta_value Meta value to be updated. */ public function save_meta_in_order_tokens( $subscription, $table, $meta_key, $meta_value ) { if ( self::$payment_method_meta_table !== $table || self::$payment_method_meta_key !== $meta_key ) { return; } $token = WC_Payment_Tokens::get( $meta_value ); if ( empty( $token ) ) { return; } $this->add_token_to_order( $subscription, $token ); } /** * Loads the subscription edit page script with user cards to hijack the payment method input and * transform it into a select element. * * @param WC_Order $order The WC Order. */ public function add_payment_method_select_to_subscription_edit( $order ) { // Do not load the script if the order is not a subscription. if ( ! wcs_is_subscription( $order ) ) { return; } WC_Payments::register_script_with_dependencies( 'WCPAY_SUBSCRIPTION_EDIT_PAGE', 'dist/subscription-edit-page' ); wp_set_script_translations( 'WCPAY_SUBSCRIPTION_EDIT_PAGE', 'woocommerce-payments' ); wp_enqueue_script( 'WCPAY_SUBSCRIPTION_EDIT_PAGE' ); } /** * Render the payment method used for a subscription in My Account pages * * @param string $payment_method_to_display Default payment method to display. * @param WC_Subscription $subscription Subscription object. * * @return string Payment method string to display in UI. */ public function maybe_render_subscription_payment_method( $payment_method_to_display, $subscription ) { try { if ( $subscription->get_payment_method() !== $this->id ) { return $payment_method_to_display; } $token = $this->get_payment_token( $subscription ); if ( is_null( $token ) ) { Logger::info( 'There is no saved payment token for subscription #' . $subscription->get_id() ); } else { $payment_method_to_display = $token->get_display_name(); } return $payment_method_to_display; } catch ( \Exception $e ) { Logger::error( 'Failed to get payment method for subscription #' . $subscription->get_id() . ' ' . $e ); return $payment_method_to_display; } } /** * Hide "Change payment" button for manual subscriptions with non-reusable payment methods. * These subscriptions use the "Renew now" flow where customers choose a payment method at renewal time. * * @param array $actions The subscription actions. * @param WC_Subscription $subscription The subscription object. * @return array The modified actions array. */ public function maybe_hide_change_payment_for_manual_subscriptions( $actions, $subscription ) { // Only process manual subscriptions with non-reusable payment methods. $original_payment_method_id = $subscription->get_meta( '_wcpay_original_payment_method_id', true ); if ( $subscription->is_manual() && ! empty( $original_payment_method_id ) ) { // Remove the "Change payment" action since they'll choose payment method during renewal. unset( $actions['change_payment_method'] ); } return $actions; } /** * Hide "Auto renew" toggle for manual subscriptions with non-reusable payment methods. * * @param array $allcaps List of user capabilities. * @param array $caps Which capabilities are being checked. * @param array $args Arguments, in our case user ID and subscription ID. * @return array */ public function maybe_hide_auto_renew_toggle_for_manual_subscriptions( $allcaps, $caps, $args ) { if ( ! isset( $caps[0] ) || 'toggle_shop_subscription_auto_renewal' !== $caps[0] ) { // Do not interfere with other capabilities. return $allcaps; } if ( ! isset( $args[2] ) ) { return $allcaps; } $subscription = wcs_get_subscription( $args[2] ); if ( ! $subscription ) { return $allcaps; } // Only process manual subscriptions with non-reusable payment methods. $original_payment_method_id = $subscription->get_meta( '_wcpay_original_payment_method_id', true ); if ( $subscription->is_manual() && ! empty( $original_payment_method_id ) ) { // Remove the capability as this subscription won't work with automatic renewals. unset( $allcaps['toggle_shop_subscription_auto_renewal'] ); } return $allcaps; } /** * AJAX handler to fetch payment tokens for a user. * * @return void */ public function ajax_get_user_payment_tokens() { check_ajax_referer( 'wcpay-subscription-edit', 'nonce' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( [ 'message' => __( 'You do not have permission to perform this action.', 'woocommerce-payments' ) ], 403 ); return; } $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0; if ( $user_id <= 0 ) { wp_send_json_success( [ 'tokens' => [] ] ); return; } // Verify user exists. $user = get_user_by( 'id', $user_id ); if ( ! $user ) { wp_send_json_error( [ 'message' => __( 'Invalid user ID.', 'woocommerce-payments' ) ], 400 ); return; } $tokens = $this->get_user_formatted_tokens_array( $user_id ); wp_send_json_success( [ 'tokens' => $tokens ] ); } /** * Outputs a select element to be used for the Subscriptions payment meta token selection. * * @param WC_Subscription $subscription The subscription object. * @param string $field_id The field_id to add to the select element. * @param string $field_value The field_value to be selected by default. */ public function render_custom_payment_meta_input( $subscription, $field_id, $field_value ) { // Make sure that we are either working with integers or null. $field_value = ctype_digit( $field_value ) ? absint( $field_value ) : ( is_int( $field_value ) ? $field_value : null ); $user_id = $subscription->get_user_id(); $disabled = false; $selected = null; $options = []; $prepared_data = [ 'value' => $field_value, 'userId' => $user_id, 'tokens' => [], 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wcpay-subscription-edit' ), ]; if ( $user_id > 0 ) { $tokens = $this->get_user_formatted_tokens_array( $user_id ); foreach ( $tokens as $token ) { $options[ $token['tokenId'] ] = $token['displayName']; if ( $field_value === $token['tokenId'] || ( ! $field_value && $token['isDefault'] ) ) { $selected = $token['tokenId']; } } $prepared_data['tokens'] = $tokens; if ( empty( $options ) ) { $options[0] = __( 'No payment methods found for customer', 'woocommerce-payments' ); $disabled = true; } } else { $options[0] = __( 'Please select a customer first', 'woocommerce-payments' ); $selected = 0; $disabled = true; } ?> <span class="wcpay-subscription-payment-method" data-wcpay-pm-selector="<?php echo esc_attr( wp_json_encode( $prepared_data ) ); ?>"> <select name="<?php echo esc_attr( $field_id ); ?>" id="<?php echo esc_attr( $field_id ); ?>"> <?php if ( $field_value && $field_value !== $selected ) : ?> <option value="" selected disabled><?php echo esc_html__( 'Please select a payment method', 'woocommerce-payments' ); ?></option> <?php endif; ?> <?php foreach ( $options as $token_id => $display_name ) : ?> <option value="<?php echo esc_attr( $token_id ); ?>" <?php selected( $token_id, $selected ); ?> <?php echo disabled( $disabled ); ?>> <?php echo esc_html( $display_name ); ?> </option> <?php endforeach; ?> </select> </span> <?php } /** * Add specific data like last 4 digit of wcpay payment gateway * * @param string $old_payment_method_title Payment method title, eg: Credit card. * @param string $old_payment_method Payment gateway id. * @param WC_Subscription $subscription The subscription order. * @return string */ public function get_specific_old_payment_method_title( $old_payment_method_title, $old_payment_method, $subscription ) { // make sure payment method is wcpay's. if ( WC_Payment_Gateway_WCPay::GATEWAY_ID !== $old_payment_method ) { return $old_payment_method_title; } if ( $this->is_changing_payment_method_for_subscription() ) { $token_ids = $subscription->get_payment_tokens(); // since old payment must be the second to last saved payment... if ( count( $token_ids ) < 2 ) { return $old_payment_method_title; } $second_to_last_token_id = $token_ids[ count( $token_ids ) - 2 ]; $token = WC_Payment_Tokens::get( $second_to_last_token_id ); if ( $token && $token instanceof WC_Payment_Token_CC ) { // translators: 1: payment method likely credit card, 2: last 4 digit. return sprintf( __( '%1$s ending in %2$s', 'woocommerce-payments' ), $old_payment_method_title, $token->get_last4() ); } } else { $last_order_id = $subscription->get_last_order(); if ( ! $last_order_id ) { return $old_payment_method_title; } $last_order = wc_get_order( $last_order_id ); $token_ids = $last_order->get_payment_tokens(); // since old payment must be the second to last saved payment... if ( count( $token_ids ) < 2 ) { return $old_payment_method_title; } $second_to_last_token_id = $token_ids[ count( $token_ids ) - 2 ]; $token = WC_Payment_Tokens::get( $second_to_last_token_id ); if ( $token && $token instanceof WC_Payment_Token_CC ) { // translators: 1: payment method likely credit card, 2: last 4 digit. return sprintf( __( '%1$s ending in %2$s', 'woocommerce-payments' ), $old_payment_method_title, $token->get_last4() ); } } return $old_payment_method_title; } /** * Add specific data like last 4 digit of wcpay payment gateway * * @param string $new_payment_method_title Payment method title, eg: Credit card. * @param string $new_payment_method Payment gateway id. * @param WC_Subscription $subscription The subscription order. * @return string */ public function get_specific_new_payment_method_title( $new_payment_method_title, $new_payment_method, $subscription ) { // make sure payment method is wcpay's. if ( WC_Payment_Gateway_WCPay::GATEWAY_ID !== $new_payment_method ) { return $new_payment_method_title; } if ( $this->is_changing_payment_method_for_subscription() ) { $order = $subscription; } else { $last_order_id = $subscription->get_last_order(); if ( ! $last_order_id ) { return $new_payment_method_title; } $order = wc_get_order( $last_order_id ); } try { $payment_information = $this->prepare_payment_information( $order ); } catch ( Exception $e ) { return $new_payment_method_title; } if ( $payment_information->is_using_saved_payment_method() ) { $token = $payment_information->get_payment_token(); if ( $token && $token instanceof WC_Payment_Token_CC ) { // translators: 1: payment method likely credit card, 2: last 4 digit. return sprintf( __( '%1$s ending in %2$s', 'woocommerce-payments' ), $new_payment_method_title, $token->get_last4() ); } } else { try { $payment_method_id = $payment_information->get_payment_method(); $payment_method = $this->payments_api_client->get_payment_method( $payment_method_id ); if ( ! empty( $payment_method['card']['last4'] ) ) { // translators: 1: payment method likely credit card, 2: last 4 digit. return sprintf( __( '%1$s ending in %2$s', 'woocommerce-payments' ), $new_payment_method_title, $payment_method['card']['last4'] ); } } catch ( Exception $e ) { Logger::error( $e ); } } return $new_payment_method_title; } /** * When an order is created/updated, we want to add an ActionScheduler job to send this data to * the payment server. * * @param int $order_id The ID of the order that has been created. * @param WC_Order|null $order The order that has been created. * * @throws Order_Not_Found_Exception */ public function maybe_schedule_subscription_order_tracking( $order_id, $order = null ) { if ( ! $this->is_subscriptions_enabled() ) { return; } $save_meta_data = false; if ( is_null( $order ) ) { $order = wc_get_order( $order_id ); } $payment_token = $this->get_payment_token( $order ); // If we can't get the payment token for this order, then we check if we already have a payment token // set in the order metadata. If we don't, then we try and get the parent order's token from the metadata. if ( is_null( $payment_token ) ) { if ( empty( $this->order_service->get_payment_method_id_for_order( $order ) ) ) { $parent_order = wc_get_order( $order->get_parent_id() ); if ( $parent_order ) { $parent_payment_method_id = $this->order_service->get_payment_method_id_for_order( $parent_order ); } // If there is no parent order, or the parent order doesn't have the metadata set, then we cannot track this order. if ( empty( $parent_order ) || empty( $parent_payment_method_id ) ) { return; } $this->order_service->set_payment_method_id_for_order( $order, $parent_payment_method_id ); $save_meta_data = true; } } elseif ( $this->order_service->get_payment_method_id_for_order( $order ) !== $payment_token->get_token() ) { // If the payment token stored in the metadata already doesn't reflect the latest token, update it. $this->order_service->set_payment_method_id_for_order( $order, $payment_token->get_token() ); $save_meta_data = true; } // If the stripe customer ID metadata isn't set for this order, try and get this data from the metadata of the parent order. if ( empty( $this->order_service->get_customer_id_for_order( $order ) ) ) { $parent_order = wc_get_order( $order->get_parent_id() ); if ( $parent_order ) { $parent_customer_id = $this->order_service->get_customer_id_for_order( $parent_order ); } if ( ! empty( $parent_order ) && ! empty( $parent_customer_id ) ) { $this->order_service->set_customer_id_for_order( $order, $parent_customer_id ); $save_meta_data = true; } } // If we need to, save our changes to the metadata for this order. if ( $save_meta_data ) { $order->save_meta_data(); } } /** * Action called when a renewal order is created, allowing us to strip metadata that we do not * want it to inherit from the parent order. * * @param string $order_meta_query The metadata query (a valid SQL query). * @param int $to_order The renewal order. * @param int $from_order The source (parent) order. * * @return string */ public function update_renewal_meta_data( $order_meta_query, $to_order, $from_order ) { $order_meta_query .= " AND `meta_key` NOT IN ('_new_order_tracking_complete')"; return $order_meta_query; } /** * Removes the data that we don't need to copy to renewal orders. * * @param array $order_data Renewal order data. * * @return array The renewal order data with the data we don't want copied removed */ public function remove_data_renewal_order( $order_data ) { unset( $order_data['_new_order_tracking_complete'] ); return $order_data; } /** * Adds the failed SCA auth email to WooCommerce. * * @param WC_Email[] $email_classes All existing emails. * @return WC_Email[] */ public function add_emails( $email_classes ) { include_once __DIR__ . '/class-wc-payments-email-failed-renewal-authentication.php'; include_once __DIR__ . '/class-wc-payments-email-failed-authentication-retry.php'; $email_classes['WC_Payments_Email_Failed_Renewal_Authentication'] = new WC_Payments_Email_Failed_Renewal_Authentication( $email_classes ); $email_classes['WC_Payments_Email_Failed_Authentication_Retry'] = new WC_Payments_Email_Failed_Authentication_Retry(); return $email_classes; } /** * If this is the "Pass the SCA challenge" flow, remove a variable that is checked by WC Subscriptions * so WC Subscriptions doesn't redirect to the checkout */ public function remove_order_pay_var() { global $wp; if ( isset( $_GET['wcpay-confirmation'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $this->order_pay_var = $wp->query_vars['order-pay']; $wp->query_vars['order-pay'] = null; } } /** * Restore the variable that was removed in remove_order_pay_var() */ public function restore_order_pay_var() { global $wp; if ( isset( $this->order_pay_var ) ) { $wp->query_vars['order-pay'] = $this->order_pay_var; } } /** * Update the specified subscription's payment token with a new token. * * @param bool $updated Whether the token was updated. * @param WC_Subscription $subscription The subscription whose payment token need to be updated. * @param WC_Payment_Token $new_token The new payment token to be used for the specified subscription. * * @return bool Whether this function updates the token or not. */ public function update_subscription_token( $updated, $subscription, $new_token ) { if ( $this->id !== $new_token->get_gateway_id() ) { return $updated; } $subscription->set_payment_method( $this->id ); $subscription->update_meta_data( '_payment_method_id', $new_token->get_token() ); $subscription->add_payment_token( $new_token ); $subscription->save(); return true; } /** * Checks if a renewal order is linked to a WCPay subscription. * * @param WC_Order $renewal_order The renewal order to check. * * @return bool True if the renewal order is linked to a renewal order. Otherwise false. */ private function is_wcpay_subscription_renewal_order( WC_Order $renewal_order ) { // Renewal orders copy metadata from the parent subscription, so we can first check if it has the `_wcpay_subscription_id` meta. if ( ! class_exists( 'WC_Payments_Subscription_Service' ) || ! $renewal_order->meta_exists( WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY ) ) { return false; } // Confirm the renewal order is linked to a subscription which is a WCPay Subscription. foreach ( wcs_get_subscriptions_for_renewal_order( $renewal_order ) as $subscription ) { if ( WC_Payments_Subscription_Service::is_wcpay_subscription( $subscription ) ) { return true; } } return false; } /** * Get card mandate parameters for the order payment intent if needed. * Only required for subscriptions creation for cards issued in India. * More details https://wp.me/pc4etw-ky * * @param WC_Order $order The subscription order. * @return array Params to be included or empty array. */ public function get_mandate_params_for_order( WC_Order $order ): array { $result = []; if ( ! $this->is_subscriptions_enabled() ) { return $result; } $subscriptions = wcs_get_subscriptions_for_order( $order->get_id() ); $subscription = reset( $subscriptions ); if ( ! $subscription ) { return $result; } // TEMP Fix – Stripe validates mandate params for cards not // issued by Indian banks. Apply them only for INR as Indian banks // only support it for now. $currency = $order->get_currency(); if ( 'INR' !== $currency ) { return $result; } // Get total by adding only subscriptions and get rid of any other product or fee. $subs_amount = 0.0; foreach ( $subscriptions as $sub ) { $subs_amount += $sub->get_total(); } $amount = WC_Payments_Utils::prepare_amount( $subs_amount, $order->get_currency() ); // TEMP Fix – Prevent stale free subscription data to throw // an error due amount < 1. if ( 0 === $amount ) { return $result; } $result['setup_future_usage'] = 'off_session'; $result['payment_method_options']['card']['mandate_options'] = [ 'reference' => $order->get_id(), 'amount' => $amount, 'amount_type' => 'fixed', 'start_date' => $subscription->get_time( 'date_created' ), 'interval' => $subscription->get_billing_period(), 'interval_count' => $subscription->get_billing_interval(), 'supported_types' => [ 'india' ], ]; // Multiple subscriptions per order needs: // - Set amount type to maximum, to allow renews of any amount under the order total. // - Set interval to sporadic, to not follow any specific interval. // - Unset interval count, because it doesn't apply anymore. if ( 1 < count( $subscriptions ) ) { $result['card']['mandate_options']['amount_type'] = 'maximum'; $result['card']['mandate_options']['interval'] = 'sporadic'; if ( isset( $result['card']['mandate_options']['interval_count'] ) ) { unset( $result['card']['mandate_options']['interval_count'] ); } } return $result; } /** * Add an order note if the renew intent customer notification requires the merchant to authenticate the payment. * The note includes the charge attempt date and let the merchant know the need of an off-session step by the customer. * * @param WC_Order $order The renew order. * @param array $processing Processing state from Stripe's intent response. * @return void */ public function maybe_add_customer_notification_note( WC_Order $order, array $processing = [] ) { $approval_requested = $processing['card']['customer_notification']['approval_requested'] ?? false; $completes_at = $processing['card']['customer_notification']['completes_at'] ?? null; if ( $approval_requested && $completes_at ) { $attempt_date = wp_date( get_option( 'date_format', 'F j, Y' ), $completes_at, wp_timezone() ); $attempt_time = wp_date( get_option( 'time_format', 'g:i a' ), $completes_at, wp_timezone() ); $note = sprintf( /* translators: 1) date in date_format or 'F j, Y'; 2) time in time_format or 'g:i a' */ __( 'The customer must authorize this payment via a notification sent to them by the bank which issued their card. The authorization must be completed before %1$s at %2$s, when the charge will be attempted.', 'woocommerce-payments' ), $attempt_date, $attempt_time ); $order->add_order_note( $note ); } } /** * Get mandate ID parameter to renewal payment if exists. * Only required for subscriptions renewals for cards issued in India. * More details https://wp.me/pc4etw-ky * * @param WC_Order $renewal_order The subscription renewal order. * @return string Param to be included or empty array. */ public function get_mandate_param_for_renewal_order( WC_Order $renewal_order ): string { $subscriptions = wcs_get_subscriptions_for_renewal_order( $renewal_order->get_id() ); $subscription = reset( $subscriptions ); if ( ! $subscription ) { return ''; } $parent_order = wc_get_order( $subscription->get_parent_id() ); if ( ! $parent_order ) { return ''; } $mandate = $parent_order->get_meta( '_stripe_mandate_id', true ); if ( empty( $mandate ) ) { return ''; } return $mandate; } /** * Force subscription to manual renewal if non-reusable payment method was used. * This should be hooked into 'woocommerce_checkout_subscription_created' action. * * @param WC_Subscription $subscription The subscription being created. */ public function maybe_force_subscription_to_manual( $subscription ) { // Only process WCPay subscriptions (including split UPE gateways like woocommerce_payments_ideal). $payment_method_id = $subscription->get_payment_method(); if ( 0 !== strpos( $payment_method_id, WC_Payment_Gateway_WCPay::GATEWAY_ID ) ) { return; } // Check if this is a split UPE gateway (e.g., woocommerce_payments_ideal). // Split UPE gateways are used for non-reusable payment methods like iDEAL, Bancontact, etc. // The base gateway (woocommerce_payments) is used for cards, which are reusable. if ( WC_Payment_Gateway_WCPay::GATEWAY_ID === $payment_method_id ) { // This is the base gateway (card), which is reusable - no action needed. return; } // This is a split UPE gateway (non-reusable payment method). // Extract the payment method type from the gateway ID (e.g., "ideal" from "woocommerce_payments_ideal"). $payment_method_type = str_replace( WC_Payment_Gateway_WCPay::GATEWAY_ID . '_', '', $payment_method_id ); // Store the original payment method ID for reference. $subscription->update_meta_data( '_wcpay_original_payment_method_id', $payment_method_id ); // Set to manual renewal (keep the original split payment method ID). $subscription->set_requires_manual_renewal( true ); $subscription->save(); // Add order note confirming the subscription was set to manual. $subscription->add_order_note( sprintf( /* translators: %s: payment method type */ __( 'Subscription set to manual renewal because %s is a non-reusable payment method.', 'woocommerce-payments' ), $payment_method_type ) ); } } includes/compat/blocks/class-blocks-data-extractor.php 0000777 00000007536 15251156605 0017153 0 ustar 00 <?php /** * Class Blocks_Data_Extractor * * @package WooCommerce\Payments */ namespace WCPay; use Automattic\WooCommerce\StoreApi\StoreApi; use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CheckoutSchema; use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry; defined( 'ABSPATH' ) || exit; // block direct access. /** * Extract data fields from certain block based plugins. */ class Blocks_Data_Extractor { /** * Instance of the integration registry. * * @var IntegrationRegistry */ private $integration_registry; /** * Constructor. */ public function __construct() { $this->integration_registry = new IntegrationRegistry(); } /** * Get a list of available Blocks. * * @return array */ private function get_available_blocks() { $blocks = []; if ( class_exists( '\AutomateWoo\Blocks\Marketing_Optin_Block' ) ) { $blocks[] = new \Automatewoo\Blocks\Marketing_Optin_Block(); } if ( class_exists( '\Mailchimp_Woocommerce_Newsletter_Blocks_Integration' ) ) { $blocks[] = new \Mailchimp_Woocommerce_Newsletter_Blocks_Integration(); } if ( class_exists( '\WCK\Blocks\CheckoutIntegration' ) ) { $blocks[] = new \WCK\Blocks\CheckoutIntegration(); } return $blocks; } /** * Register all the blocks. * * @param array $blocks A list of blocks to register. * @return void */ private function register_blocks( $blocks ) { foreach ( $blocks as $block ) { $this->integration_registry->register( $block ); } } /** * Unregister all blocks. * * @param array $blocks A list of blocks to unregister. * @return void */ private function unregister_blocks( $blocks ) { foreach ( $blocks as $block ) { $this->integration_registry->unregister( $block ); } } /** * Mailpoet's block registration is different from the other two plugins. Data fields are passed * from the parent class. This method fetches the data fields without registering the plugin. * * @return array */ private function get_mailpoet_data() { $mailpoet_wc_subscription = \MailPoet\DI\ContainerWrapper::getInstance()->get( \MailPoet\WooCommerce\Subscription::class ); $settings_instance = \MailPoet\Settings\SettingsController::getInstance(); $settings = [ 'defaultText' => $settings_instance->get( 'woocommerce.optin_on_checkout.message', '' ), 'optinEnabled' => $settings_instance->get( 'woocommerce.optin_on_checkout.enabled', false ), 'defaultStatus' => false, ]; if ( version_compare( \MAILPOET_VERSION, '4.18.0', '<=' ) ) { $settings['defaultStatus'] = $mailpoet_wc_subscription->isCurrentUserSubscribed(); } return $settings; } /** * Retrieve data fields. * * @return array */ public function get_data() { $blocks = $this->get_available_blocks(); $this->register_blocks( $blocks ); $blocks_data = $this->integration_registry->get_all_registered_script_data(); if ( class_exists( 'MailPoet\DI\ContainerWrapper' ) && class_exists( 'MailPoet\WooCommerce\Subscription' ) ) { $blocks_data += [ 'mailpoet_data' => $this->get_mailpoet_data() ]; } $this->unregister_blocks( $blocks ); return $blocks_data; } /** * Retrieves the namespaces in the Store API checkout schema. * * @return array */ public function get_checkout_schema_namespaces(): array { $namespaces = []; if ( class_exists( 'Automattic\WooCommerce\StoreApi\StoreApi' ) && class_exists( 'Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema' ) && class_exists( 'Automattic\WooCommerce\Blocks\StoreApi\Schemas\CheckoutSchema' ) ) { try { $checkout_schema = StoreApi::container()->get( ExtendSchema::class )->get_endpoint_schema( CheckoutSchema::IDENTIFIER ); } catch ( \Exception $e ) { return $namespaces; } $namespaces = array_keys( (array) $checkout_schema ); } return $namespaces; } } includes/compat/multi-currency/class-wc-payments-currency-manager.php 0000777 00000013514 15251156605 0022163 0 ustar 00 <?php /** * Class WC_Payments_Currency_Manager * * @package WooCommerce\Payments */ namespace WCPay; use WC_Payment_Gateway_WCPay; defined( 'ABSPATH' ) || exit; /** * It ensures that when a payment method is added and multi-currency is enabled, the needed currency is also added. */ class WC_Payments_Currency_Manager { /** * The WCPay gateway class instance. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * Constructor * * @param WC_Payment_Gateway_WCPay $gateway The WCPay gateway class instance. */ public function __construct( WC_Payment_Gateway_WCPay $gateway ) { $this->gateway = $gateway; } /** * Initializes this class' WP hooks. * * @return void */ public function init_hooks() { add_action( 'update_option_woocommerce_woocommerce_payments_settings', [ $this, 'maybe_add_missing_currencies' ] ); add_action( 'admin_head', [ $this, 'add_payment_method_currency_dependencies_script' ] ); } /** * Gets the multi-currency instance or returns null if it's not available. * This method allows for easier testing by allowing the multi-currency instance to be mocked. * * @return \WCPay\MultiCurrency\MultiCurrency|null */ public function get_multi_currency_instance() { if ( ! function_exists( 'WC_Payments_Multi_Currency' ) ) { return null; } if ( ! WC_Payments_Multi_Currency()->is_initialized() ) { return null; } return WC_Payments_Multi_Currency(); } /** * Returns the currencies needed per enabled payment method * * @return array The currencies keyed with the related payment method */ public function get_enabled_payment_method_currencies() { $enabled_payment_method_ids = $this->gateway->get_upe_enabled_payment_method_ids(); // getting all the payment methods that are also present in `$enabled_payment_method_ids`. $enabled_payment_methods = array_values( array_intersect_key( $this->gateway->wc_payments_get_payment_method_map(), array_flip( $enabled_payment_method_ids ) ) ); $account_currency = $this->gateway->get_account_domestic_currency(); $payment_methods_needing_currency = array_reduce( $enabled_payment_methods, function ( $result, $payment_method_instance ) use ( $account_currency ) { $method = $payment_method_instance->get_id(); if ( in_array( $method, [ 'card', 'card_present', 'link' ], true ) ) { return $result; } $result[ $method ] = [ 'currencies' => $payment_method_instance->has_domestic_transactions_restrictions() ? [ $account_currency ] : $payment_method_instance->get_currencies(), 'title' => $payment_method_instance->get_title( $this->gateway->get_account_country() ), ]; return $result; }, [] ); return $payment_methods_needing_currency; } /** * Ensures that when a payment method is added from the settings, the needed currency is also added. */ public function maybe_add_missing_currencies() { $multi_currency = $this->get_multi_currency_instance(); if ( is_null( $multi_currency ) ) { return; } $payment_methods_needing_currency = $this->get_enabled_payment_method_currencies(); if ( empty( $payment_methods_needing_currency ) ) { return; } $enabled_currencies = $multi_currency->get_enabled_currencies(); $available_currencies = $multi_currency->get_available_currencies(); $missing_currency_codes = []; // TODO: we need to find something about having a currency not available for the method in case of having disabled currencies in the future. // First option, not do display it if the available currency is blocked by something else (Stripe, merchant, WCPay etc.) // Second option, showing a notice that it can't be selected because the currency is not available to use. // we have payments needing some currency being enabled, let's ensure the currency is present. foreach ( $payment_methods_needing_currency as $payment_method_data ) { $needed_currency_codes = $payment_method_data['currencies']; foreach ( $needed_currency_codes as $needed_currency_code ) { if ( ! isset( $available_currencies[ $needed_currency_code ] ) ) { continue; } if ( isset( $enabled_currencies[ $needed_currency_code ] ) ) { continue; } $missing_currency_codes[] = $needed_currency_code; } } $missing_currency_codes = array_unique( $missing_currency_codes ); if ( empty( $missing_currency_codes ) ) { return; } /** * The set_enabled_currencies method throws an exception if any currencies passed are not found in the current available currencies. * Any currencies not found are filtered out above, so we shouldn't need a try/catch here. */ $multi_currency->set_enabled_currencies( array_merge( array_keys( $enabled_currencies ), $missing_currency_codes ) ); } /** * Adds the `multiCurrencyPaymentMethodsMap` JS object to the multi-currency settings page. * * This object maps currencies to payment methods that require them, so the multi-currency settings page displays a notice in case of dependencies. */ public function add_payment_method_currency_dependencies_script() { $multi_currency = $this->get_multi_currency_instance(); if ( is_null( $multi_currency ) || ! $multi_currency->is_multi_currency_settings_page() ) { return; } $payment_methods_needing_currency = $this->get_enabled_payment_method_currencies(); if ( empty( $payment_methods_needing_currency ) ) { return; } $currency_methods_map = []; foreach ( $payment_methods_needing_currency as $method => $data ) { foreach ( $data['currencies'] as $currency ) { if ( ! isset( $currency_methods_map[ $currency ] ) ) { $currency_methods_map[ $currency ] = []; } $currency_methods_map[ $currency ][ $method ] = $data['title']; } } ?> <script type='text/javascript'> window.multiCurrencyPaymentMethodsMap = <?php echo wp_json_encode( $currency_methods_map ); ?>; </script> <?php } } includes/compat/multi-currency/wc-payments-multi-currency.php 0000777 00000004061 15251156605 0020575 0 ustar 00 <?php /** * Main functions to start MultiCurrency class. * * @package WooCommerce\Payments */ defined( 'ABSPATH' ) || exit; /** * Load customer multi-currency if feature is enabled or if it is the setup page. */ function wcpay_multi_currency_onboarding_check() { $is_setup_page = false; // Skip checking the HTTP referer if it is a cron job. if ( ! defined( 'DOING_CRON' ) ) { $http_referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); if ( ! empty( $http_referer ) ) { $is_setup_page = strpos( $http_referer, 'multi-currency-setup' ) !== false; } } return $is_setup_page; } if ( ! WC_Payments_Features::is_customer_multi_currency_enabled() && ! wcpay_multi_currency_onboarding_check() ) { return; } /** * Returns the MultiCurrency singleton. * * @return WCPay\MultiCurrency\MultiCurrency */ function WC_Payments_Multi_Currency() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid static $instance = null; if ( is_null( $instance ) ) { $instance = new WCPay\MultiCurrency\MultiCurrency( WC_Payments::get_settings_service(), WC_Payments::get_payments_api_client(), WC_Payments::get_account_service(), WC_Payments::get_localization_service(), WC_Payments::get_database_cache() ); $instance->init_hooks(); } return $instance; } add_action( 'plugins_loaded', 'WC_Payments_Multi_Currency', 12 ); register_deactivation_hook( WCPAY_PLUGIN_FILE, 'wcpay_multi_currency_deactivated' ); /** * Plugin deactivation hook. */ function wcpay_multi_currency_deactivated() { WCPay\MultiCurrency\MultiCurrency::remove_woo_admin_notes(); } if ( ! function_exists( 'wc_get_currency_switcher_markup' ) ) { /** * Gets the switcher widget markup. * * @param array $instance The widget's instance settings. * @param array $args The widget's arguments. * * @return string The widget markup. */ function wc_get_currency_switcher_markup( array $instance = [], array $args = [] ): string { return WC_Payments_Multi_Currency()->get_switcher_widget_markup( $instance, $args ); } } includes/class-wc-payments-woopay-button-handler.php 0000777 00000030665 15251156605 0016724 0 ustar 00 <?php /** * Class WC_Payments_WooPay_Button_Handler * Adds support for the WooPay express checkout button. * * Borrowed heavily from the WC_Payments_Payment_Request_Button_Handler class. * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; } // TODO: Not sure which of these are needed yet. use WCPay\Exceptions\Invalid_Price_Exception; use WCPay\Logger; use WCPay\Payment_Information; use WCPay\WooPay\WooPay_Session; use WCPay\WooPay\WooPay_Utilities; /** * WC_Payments_WooPay_Button_Handler class. */ class WC_Payments_WooPay_Button_Handler { /** * WC_Payments_Account instance to get information about the account * * @var WC_Payments_Account */ private $account; /** * WC_Payment_Gateway_WCPay instance. * * @var WC_Payment_Gateway_WCPay */ private $gateway; /** * WooPay_Utilities instance. * * @var WooPay_Utilities */ private $woopay_utilities; /** * Express Checkout Helper instance. * * @var WC_Payments_Express_Checkout_Button_Helper */ private $express_checkout_helper; /** * Initialize class actions. * * @param WC_Payments_Account $account Account information. * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. * @param WooPay_Utilities $woopay_utilities WCPay gateway. * @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper Express checkout helper. */ public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway, WooPay_Utilities $woopay_utilities, WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper ) { $this->account = $account; $this->gateway = $gateway; $this->woopay_utilities = $woopay_utilities; $this->express_checkout_helper = $express_checkout_helper; } /** * Indicates eligibility for WooPay via feature flag. * * @var bool */ private $is_woopay_eligible; /** * Indicates whether WooPay is enabled. * * @var bool */ private $is_woopay_enabled; /** * Indicates whether WooPay express checkout is enabled. * * @var bool */ private $is_woopay_express_button_enabled; /** * Indicates whether WooPay and WooPay express checkout are enabled. * * @return bool */ public function is_woopay_enabled() { /** * Allows 3PD to programmatically show/hide the WooPay button. * * @since 9.5.0 */ return apply_filters( 'wcpay_woopay_enabled', $this->is_woopay_eligible && $this->is_woopay_enabled && $this->is_woopay_express_button_enabled ); } /** * Initialize hooks. * * @return void */ public function init() { // Checks if WCPay is enabled. if ( ! $this->gateway->is_enabled() ) { return; } // Checks if WooPay is enabled. $this->is_woopay_eligible = WC_Payments_Features::is_woopay_eligible(); // Feature flag. $this->is_woopay_enabled = 'yes' === $this->gateway->get_option( 'platform_checkout', 'no' ); $this->is_woopay_express_button_enabled = WC_Payments_Features::is_woopay_express_checkout_enabled(); if ( ! $this->is_woopay_enabled() ) { return; } // Don't load for change payment method page. if ( isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return; } add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] ); add_filter( 'wcpay_payment_fields_js_config', [ $this, 'add_woopay_config' ] ); add_action( 'wp_ajax_woopay_express_checkout_button_show_error_notice', [ $this, 'show_error_notice' ] ); add_action( 'wp_ajax_nopriv_woopay_express_checkout_button_show_error_notice', [ $this, 'show_error_notice' ] ); } /** * Add the woopay button config to wcpay_config. * * @param array $config The existing config array. * * @return array The modified config array. */ public function add_woopay_config( $config ) { $user = wp_get_current_user(); $config['woopayButton'] = $this->get_button_settings(); $config['woopayButtonNonce'] = wp_create_nonce( 'woopay_button_nonce' ); $config['addToCartNonce'] = wp_create_nonce( 'wcpay-add-to-cart' ); $config['shouldShowWooPayButton'] = $this->should_show_woopay_button(); $config['woopaySessionEmail'] = WooPay_Session::get_user_email( $user ); $config['woopayIsCountryAvailable'] = $this->woopay_utilities->is_country_available( $this->gateway ); return $config; } /** * Load public scripts and styles. */ public function scripts() { // Don't load scripts if we should not show the button. if ( ! $this->should_show_woopay_button() ) { return; } WC_Payments::register_script_with_dependencies( 'WCPAY_WOOPAY_EXPRESS_BUTTON', 'dist/woopay-express-button' ); $wcpay_config = rawurlencode( wp_json_encode( WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() ) ); wp_add_inline_script( 'WCPAY_WOOPAY_EXPRESS_BUTTON', " var wcpayConfig = wcpayConfig || JSON.parse( decodeURIComponent( '" . esc_js( $wcpay_config ) . "' ) ); ", 'before' ); wp_set_script_translations( 'WCPAY_WOOPAY_EXPRESS_BUTTON', 'woocommerce-payments' ); wp_enqueue_script( 'WCPAY_WOOPAY_EXPRESS_BUTTON' ); WC_Payments_Utils::enqueue_style( 'WCPAY_WOOPAY', plugins_url( 'dist/woopay.css', WCPAY_PLUGIN_FILE ), [], WCPAY_VERSION_NUMBER, 'all' ); } /** * Returns the error notice HTML. */ public function show_error_notice() { $is_nonce_valid = check_ajax_referer( 'woopay_button_nonce', false, false ); if ( ! $is_nonce_valid ) { wp_send_json_error( __( 'You aren’t authorized to do that.', 'woocommerce-payments' ), 403 ); } $message = isset( $_POST['message'] ) ? sanitize_text_field( wp_unslash( $_POST['message'] ) ) : ''; // $message has already been translated. wc_add_notice( $message, 'error' ); $notice = wc_print_notices( true ); wp_send_json_success( [ 'notice' => $notice, ] ); wp_die(); } /** * The settings for the `button` attribute - they depend on the "grouped settings" flag value. * * @return array */ public function get_button_settings() { $common_settings = $this->express_checkout_helper->get_common_button_settings(); $woopay_button_settings = [ 'size' => $this->gateway->get_option( 'payment_request_button_size' ), 'context' => $this->express_checkout_helper->get_button_context(), ]; return array_merge( $common_settings, $woopay_button_settings ); } /** * Checks whether Payment Request Button should be available on this page. * * @return bool */ public function should_show_woopay_button() { // WCPay is not available. $gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( ! isset( $gateways['woocommerce_payments'] ) ) { return false; } // WooPay is not enabled. if ( ! $this->is_woopay_enabled() ) { return false; } // Page not supported. if ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_checkout() ) { return false; } // Check if WooPay is available in the user country. if ( ! $this->woopay_utilities->is_country_available( $this->gateway ) ) { return false; } // Product page, but not available in settings. if ( $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_express_checkout_method_enabled_at( 'product', 'woopay' ) ) { return false; } // Checkout page, but not available in settings. if ( $this->express_checkout_helper->is_checkout() && ! $this->express_checkout_helper->is_express_checkout_method_enabled_at( 'checkout', 'woopay' ) ) { return false; } // Cart page, but not available in settings. if ( $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_express_checkout_method_enabled_at( 'cart', 'woopay' ) ) { return false; } // Product page, but has unsupported product type. if ( $this->express_checkout_helper->is_product() && ! $this->is_product_supported() ) { Logger::log( 'Product page has unsupported product type ( WooPay Express button disabled )' ); return false; } // Cart has unsupported product type. if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && ! $this->has_allowed_items_in_cart() ) { Logger::log( 'Items in the cart have unsupported product type ( WooPay Express button disabled )' ); return false; } if ( ! is_user_logged_in() ) { // On product page for a subscription product, but not logged in, making WooPay unavailable. if ( $this->express_checkout_helper->is_product() ) { $current_product = wc_get_product(); if ( $current_product && $this->express_checkout_helper->is_product_subscription( $current_product ) ) { return false; } } // On cart or checkout page with a subscription product in cart, but not logged in, making WooPay unavailable. if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && class_exists( 'WC_Subscriptions_Cart' ) && WC_Subscriptions_Cart::cart_contains_subscription() ) { // Check cart for subscription products. return false; } // If guest checkout is not allowed, and customer is not logged in, disable the WooPay button. if ( ! $this->woopay_utilities->is_guest_checkout_enabled() ) { return false; } } /** * TODO: We need to do some research here and see if there are any product types that we * absolutely cannot support with WooPay at this time. There are some examples in the * `WC_Payments_Payment_Request_Button_Handler->is_product_supported()` method. */ return true; } /** * Display the payment request button. */ public function display_woopay_button_html() { if ( ! $this->should_show_woopay_button() ) { return; } $settings = $this->get_button_settings(); ?> <div id="wcpay-woopay-button" data-product_page=<?php echo esc_attr( $this->express_checkout_helper->is_product() ); ?>> <?php // The WooPay express checkout button React component will go here. This is rendered as disabled for now, until the page is initialized. ?> <button class="woopay-express-button" aria-label="<?php esc_attr_e( 'WooPay', 'woocommerce-payments' ); ?>" data-type="<?php echo esc_attr( $settings['type'] ); ?>" data-theme="<?php echo esc_attr( $settings['theme'] ); ?>" data-size="<?php echo esc_attr( $settings['size'] ); ?>" style="height: <?php echo esc_attr( $settings['height'] ); ?>px; border-radius: <?php echo esc_attr( $settings['radius'] ); ?>px" disabled ></button> </div> <?php } /** * Whether the product page has a product compatible with the WooPay Express button. * * @return boolean */ private function is_product_supported() { $product = $this->express_checkout_helper->get_product(); $is_supported = true; if ( ! is_object( $product ) ) { $is_supported = false; } // External/affiliate products are not supported. if ( is_a( $product, 'WC_Product' ) && $product->is_type( 'external' ) ) { $is_supported = false; } // Pre Orders products to be charged upon release are not supported. if ( class_exists( 'WC_Pre_Orders_Product' ) && method_exists( 'WC_Pre_Orders_Product', 'product_is_charged_upon_release' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) { $is_supported = false; } // WC Bookings require confirmation products are not supported. if ( is_a( $product, 'WC_Product_Booking' ) && method_exists( $product, 'get_requires_confirmation' ) && $product->get_requires_confirmation() ) { $is_supported = false; } return apply_filters( 'wcpay_woopay_button_is_product_supported', $is_supported, $product ); } /** * Checks the cart to see if the WooPay Express button supports all of its items. * * @todo Abstract this. This is a copy of the same method in the `WC_Payments_Payment_Request_Button_Handler` class. * * @return boolean */ private function has_allowed_items_in_cart() { $is_supported = true; // We don't support pre-order products to be paid upon release. if ( class_exists( 'WC_Pre_Orders_Cart' ) && class_exists( 'WC_Pre_Orders_Product' ) ) { if ( WC_Pre_Orders_Cart::cart_contains_pre_order() && WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) ) { $is_supported = false; } } return apply_filters( 'wcpay_platform_checkout_button_are_cart_items_supported', $is_supported ); } } dist/multi-currency-analytics.asset.php 0000777 00000000170 15251156605 0014320 0 ustar 00 <?php return array('dependencies' => array('wp-hooks', 'wp-i18n', 'wp-polyfill'), 'version' => '3e39ba1f2d6157c0b425'); dist/settings.css 0000777 00000216237 15251156605 0010111 0 ustar 00 .advanced-settings__theme-notice.advanced-settings__theme-notice{margin:0 0 1em 0}.advanced-settings__custom-font-url-wrapper{margin-bottom:1em}.settings-section__controls input[type=checkbox]:disabled{cursor:not-allowed} .wcpay-card-body.wcpay-card-body{padding:24px}.wcpay-card-body h4{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);margin-top:0;margin-bottom:24px}.wcpay-card-body h4:not(:first-child){margin-top:32px}.wcpay-card-body h4+p{margin-top:8px;margin-bottom:24px;font-size:12px;font-style:normal;color:#757575}.wcpay-card-body h4:has(+p){margin-bottom:0}.wcpay-card-body>*{margin-top:0;margin-bottom:1em}.wcpay-card-body>*:last-child{margin-bottom:0}.wcpay-card-body>*:last-child>:last-child{margin-bottom:0}.wcpay-card-body input,.wcpay-card-body select{margin:0}.wcpay-card-body ul>li:last-child{margin-bottom:0}.wcpay-card-body ul>li:last-child .components-base-control__field{margin-bottom:0}.wp-core-ui .wcpay-card-body select{max-width:none}.wcpay-card-body .components-checkbox-control input[type=checkbox]:checked:disabled{opacity:.3} .wcpay-confirmation-modal.wcpay-confirmation-modal{max-width:600px}.wcpay-confirmation-modal .components-modal__content{display:flex;flex-direction:column;padding:0 24px 24px}.wcpay-confirmation-modal .components-modal__content p{padding:0 0 1em 0;margin:0}.wcpay-confirmation-modal .components-modal__content p.no-padding{padding:0}.wcpay-confirmation-modal .components-modal__header{padding:24px}.wcpay-confirmation-modal__separator{margin:24px -24px}.wcpay-confirmation-modal__footer{display:flex;justify-content:flex-end}.wcpay-confirmation-modal__footer>*:not(:first-child){margin-left:16px} .wcpay-inline-notice.components-notice{margin:24px 0;padding:11px 0 11px 17px;border-left:none;border-radius:2px;justify-content:flex-start}.components-modal__header+.wcpay-inline-notice.components-notice,.wcpay-inline-notice.components-notice:first-child{margin-top:0}.wcpay-inline-notice.components-notice:last-child{margin-bottom:0}.wcpay-inline-notice.components-notice .wcpay-inline-notice__icon{display:flex;align-items:center;align-self:flex-start;min-height:auto;min-width:auto;margin-right:5px}.wcpay-inline-notice.components-notice .wcpay-inline-notice__icon svg{width:22px;height:22px}.wcpay-inline-notice.components-notice .components-notice__content{margin-top:2px;margin-bottom:2px}.wcpay-inline-notice.components-notice .components-notice__content a{white-space:nowrap}.wcpay-inline-notice.components-notice .wcpay-inline-notice__content__actions{padding-top:12px}.wcpay-inline-notice.components-notice a.wcpay-inline-notice__action{text-decoration:none}.wcpay-inline-notice.components-notice.is-dismissible{padding-right:12px}.wcpay-inline-notice.components-notice .components-notice__dismiss{height:24px;width:24px}.wcpay-inline-notice.components-notice .components-notice__dismiss svg{width:15px;height:15px;fill:#1e1e1e}.wcpay-inline-notice.components-notice.is-info{background-color:#f0f6fc}.wcpay-inline-notice.components-notice.is-info .wcpay-inline-notice__icon svg{fill:#0a4b78}.wcpay-inline-notice.components-notice.is-info button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #0a4b78}.wcpay-inline-notice.components-notice.is-info .wcpay-inline-notice__action{color:#0a4b78}.wcpay-inline-notice.components-notice.is-warning{background-color:#fcf9e8}.wcpay-inline-notice.components-notice.is-warning .wcpay-inline-notice__icon svg{fill:#614200}.wcpay-inline-notice.components-notice.is-warning button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #614200}.wcpay-inline-notice.components-notice.is-warning .wcpay-inline-notice__action{color:#614200}.wcpay-inline-notice.components-notice.is-error{background-color:#fcf0f1}.wcpay-inline-notice.components-notice.is-error .wcpay-inline-notice__icon svg{fill:#8a2424}.wcpay-inline-notice.components-notice.is-error button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #8a2424}.wcpay-inline-notice.components-notice.is-error .wcpay-inline-notice__action{color:#8a2424}.wcpay-inline-notice.components-notice.is-success{background-color:#edfaef}.wcpay-inline-notice.components-notice.is-success .wcpay-inline-notice__icon svg{fill:#005c12}.wcpay-inline-notice.components-notice.is-success button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #005c12}.wcpay-inline-notice.components-notice.is-success .wcpay-inline-notice__action{color:#005c12}.wcpay-inline-notice.components-notice button.wcpay-inline-notice__action.is-link{text-decoration:none;box-shadow:none} .wcpay-inline-notice.components-notice.woopayments-stripe-billing-notice{margin-top:0;margin-bottom:1em} .express-checkouts .wcpay-card-body{padding:0}.express-checkouts .express-checkouts-list .express-checkout{margin:0;padding:24px}.express-checkouts .express-checkouts-list .express-checkout .loadable-checkbox__icon .gridicons-notice-outline{fill:#f0b849;margin-bottom:-5px;margin-left:-4px}.express-checkouts .express-checkouts-list .express-checkout__label-container{display:flex;flex-direction:column}.express-checkouts .express-checkouts-list .express-checkout__text-container{display:flex}@media(max-width: 660px){.express-checkouts .express-checkouts-list .express-checkout__text-container{flex-direction:column}}.express-checkouts .express-checkouts-list .express-checkout__row{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;flex-wrap:nowrap;background:#fff}.express-checkouts .express-checkouts-list .express-checkout__notice{margin-top:16px}.express-checkouts .express-checkouts-list .express-checkout__description{color:#757575;font-size:12px;padding:1px;margin-right:17px;line-height:18px}@media(max-width: 660px){.express-checkouts .express-checkouts-list .express-checkout__description{margin-top:10px}}.express-checkouts .express-checkouts-list .express-checkout:not(:last-child){box-shadow:inset 0 -1px 0 #e8eaeb}.express-checkouts .express-checkouts-list .express-checkout__checkbox{display:flex;align-items:center;justify-content:center}.express-checkouts .express-checkouts-list .express-checkout__checkbox label{display:none}.express-checkouts .express-checkouts-list .express-checkout__checkbox .components-base-control__field{margin:0 4px 0 0}@media(max-width: 660px){.express-checkouts .express-checkouts-list .express-checkout__checkbox{align-self:baseline;margin-top:3px}}.express-checkouts .express-checkouts-list .express-checkout__icon{flex:0 0 63.69px;height:40px;margin:1px 17px 1px 1px}@media(min-width: 661px){.express-checkouts .express-checkouts-list .express-checkout__icon{display:flex}}@media(max-width: 660px){.express-checkouts .express-checkouts-list .express-checkout__icon{height:30px;flex:0;margin:0 10px 0 0}.express-checkouts .express-checkouts-list .express-checkout__icon img{height:30px}}.express-checkouts .express-checkouts-list .express-checkout__icons{display:flex}.express-checkouts .express-checkouts-list .express-checkout__icons .express-checkout__icon{flex:unset;width:29px}.express-checkouts .express-checkouts-list .express-checkout__icons .express-checkout__icon:not(:last-child){margin-right:4px}.express-checkouts .express-checkouts-list .express-checkout__icons .express-checkout__icon img{width:100%}.express-checkouts .express-checkouts-list .express-checkout__label{display:inline-block;font-size:14px;font-weight:600;line-height:20px;color:#1e1e1e;margin-bottom:4px}.express-checkouts .express-checkouts-list .express-checkout__label-mobile{display:none}@media(max-width: 660px){.express-checkouts .express-checkouts-list .express-checkout__label-mobile{display:block;align-self:flex-end}}.express-checkouts .express-checkouts-list .express-checkout__label-desktop{display:none}@media(min-width: 661px){.express-checkouts .express-checkouts-list .express-checkout__label-desktop{display:block}}.express-checkouts .express-checkouts-list .express-checkout__link{align-self:center}@media(max-width: 660px){.express-checkouts .express-checkouts-list .express-checkout__link{align-self:flex-start;margin-top:20px}}.express-checkouts .express-checkouts-list .express-checkout__subgroup{display:flex;align-items:center}.express-checkouts .express-checkouts-list .express-checkout__subgroup:not(:last-child){margin-bottom:1.4em}@media(max-width: 660px){.express-checkouts .express-checkouts-list .express-checkout__subgroup{align-items:flex-start;flex-wrap:wrap}}@media(min-width: 661px){body.woocommerce-payments-checkout-section .express-checkout__text-container{width:100%}body.woocommerce-payments-checkout-section .express-checkout__link{margin-left:auto}} @media screen and (max-width: 782px){.woocommerce-layout .woocommerce-layout__main{padding-top:16px;padding-bottom:16px}}.woocommerce-payments-page .components-card__body>*:first-child{margin-top:0}.woocommerce-payments-page .components-card__body>*:last-child{margin-bottom:0}.woocommerce-payments-page .woocommerce-table .woocommerce-table__item{white-space:nowrap}.woocommerce-payments-page .woocommerce-table .info-button{width:40px;padding-right:0}.woocommerce-payments-page .woocommerce-table .info-button>a{height:18px;fill:#8c8f94}.woocommerce-payments-page .woocommerce-table .info-button+.woocommerce-table__header{border-left:none}.woocommerce-payments-page .woocommerce-table .info-button .is-placeholder{display:none}.woocommerce-payments-page .woocommerce-table .payment-method__brand{display:block;margin:0 auto;text-align:center}.wcpay-test-mode-notice.components-notice.is-warning{margin:24px 0;padding:12px;background-color:#f5e6b3}.wcpay-test-mode-notice.components-notice.is-informational{margin:24px 0;padding:12px}.wcpay-connect-error-notice.components-notice{margin:24px 0;padding:12px}.payment-method-details{display:flex;align-items:flex-start}@media screen and (max-width: 1023px){.payment-method-details{display:block}}.payment-method-details__column{flex:0 0 50%}.payment-method-details-list-item{display:inline-flex}.payment-method-detail{display:flex;align-items:flex-start;font-size:14px;line-height:28px}.payment-method-detail__label{margin:0;color:#50575e;flex:0 0 36%;font-weight:normal;line-height:20px;padding:4px 0}@media screen and (max-width: 470px){.payment-method-detail__label{flex:0 0 40%}}.payment-method-detail__value{margin:0;color:#2c3338;line-height:20px;padding:4px 0;word-break:break-word}.components-modal__content .components-modal__header .components-button .components-tooltip{display:none}img.payment-method__icon{border:1px solid rgba(0,0,0,.25);border-radius:4px;display:block}img.payment-method__icon.no-border{border:none;border-radius:0}@media screen and (min-width: 783px){.woocommerce-filters-deposits .woocommerce-filters-advanced__fieldset,.woocommerce-filters-disputes .woocommerce-filters-advanced__fieldset,.woocommerce-filters-documents .woocommerce-filters-advanced__fieldset,.woocommerce-filters-transactions .woocommerce-filters-advanced__fieldset{display:grid;grid-template-columns:100px auto 1fr}.woocommerce-filters-deposits .components-select-control .components-input-control__container .components-select-control__input,.woocommerce-filters-disputes .components-select-control .components-input-control__container .components-select-control__input,.woocommerce-filters-documents .components-select-control .components-input-control__container .components-select-control__input,.woocommerce-filters-transactions .components-select-control .components-input-control__container .components-select-control__input{padding-right:var(--main-gap)}} .settings-section{display:flex;flex-flow:column}.settings-section:not(.settings-section:last-child){margin-bottom:70px}@media(min-width: 801px){.settings-section{flex-flow:row}}.settings-section__details{flex:0 1 auto;margin-bottom:24px;max-width:100%}@media(min-width: 801px){.settings-section__details{flex:0 0 33%;margin:0 24px 0 0;max-width:328px}}.settings-section__details h2{font-size:var(--l, 15px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);font-size:16px;line-height:24px;margin-top:0}.settings-section__details p{font-size:13px;line-height:17.89px;margin:12px 0}.settings-section__details>:last-child{margin-bottom:0}.settings-section__icons>li{display:inline-block;margin:0 5px 0 0}.settings-section__icons svg{border-radius:2px;box-shadow:0 0 0 1px #ddd;height:auto;width:38px}.settings-section__controls{flex:1 1 auto}.settings-section__controls .components-radio-control:not(:last-child),.settings-section__controls .components-base-control:not(:last-child){margin-bottom:24px} .wcpay-setup-real-payments-modal.components-modal__frame{width:512px}@media screen and (max-width: 600px){.wcpay-setup-real-payments-modal.components-modal__frame{height:fit-content;margin:auto auto;max-width:90vw}}.wcpay-setup-real-payments-modal .components-modal__content{box-sizing:border-box;max-width:600px;margin:0;padding:8px 36px 36px}.wcpay-setup-real-payments-modal .components-modal__header{position:initial;padding:24px 0 16px 0;height:auto;border:0}.wcpay-setup-real-payments-modal .components-modal__header h1{font-size:20px;line-height:28px;font-weight:400;font-weight:300;margin-bottom:8px}.wcpay-setup-real-payments-modal__title{font-size:20px;line-height:28px;font-weight:400}.wcpay-setup-real-payments-modal__content{display:flex;gap:24px;flex-direction:column;padding:12px 0 16px 0}.wcpay-setup-real-payments-modal__content__item p{line-height:20px;margin:0}.wcpay-setup-real-payments-modal__content__item-flex{display:flex;gap:16px;padding-right:24px}.wcpay-setup-real-payments-modal__content__item-flex__description{color:#757575}.wcpay-setup-real-payments-modal__content__item-flex p{line-height:20px;margin:0}.wcpay-setup-real-payments-modal__footer{display:flex;justify-content:flex-end;padding-top:24px}.wcpay-setup-real-payments-modal__footer>*:not(:first-child){margin-left:16px} .disable-confirmation-modal__payment-methods-list{margin-bottom:32px}.disable-confirmation-modal__payment-methods-list>*:not(:last-child){margin-bottom:12px} .woocommerce-payments__payment-method-icon{display:inline-flex;align-items:center;vertical-align:middle;max-width:160px}@media(min-width: 661px){.woocommerce-payments__payment-method-icon{max-width:inherit}}.woocommerce-payments__payment-method-icon__label{margin-left:4px}.woocommerce-payments__payment-method-icon svg{width:38px;height:auto}.woocommerce-payments__payment-method-icon .payment-method__icon{order:initial;margin-right:3px;max-width:45px} .payment-delete-illustration__wrapper{margin:59px auto 24px auto;width:35%}.payment-delete-illustration__illustrations{position:relative;margin:0 auto;width:100px}.payment-delete-illustration__illustrations::before{content:" ";position:absolute;top:0;left:0;bottom:0;right:0;background:no-repeat url(../assets/images/cards/unknown.svg?ver=9b703f2bcf3ded532116);transform:translate(-22%, -50%);height:78px;width:110px;background-size:110px 78px}.payment-delete-illustration__payment-icon{position:relative;width:100%;height:auto;border-radius:5%}.payment-delete-illustration__payment-icon.has-border{background:#fff;border:1px solid #ddd}.payment-delete-illustration__payment-cross-icon{position:absolute;top:-24px;right:-16px;width:35px;height:auto;fill:#cc1818;background:#fff;border-radius:999px} .wcpay-settings-layout{margin:24px auto 0;max-width:1032px;display:flex;flex-flow:column}@media(min-width: 961px){.wcpay-settings-layout{padding:0 56px}}body.woocommerce-payments-checkout-section .wcpay-settings-layout{margin:24px 0 0;max-width:1184px}@media(min-width: 961px){body.woocommerce-payments-checkout-section .wcpay-settings-layout{padding:0 30px}} .save-settings-section{text-align:right} @media(min-width: 960px){.woopay-disable-feedback{max-height:calc(100% - 120px)}}.woopay-disable-feedback .components-modal__content{padding:0}.woopay-disable-feedback-iframe{width:100%;height:100%}@media(min-width: 600px){.woopay-disable-feedback-iframe{width:600px;height:650px}}.woopay-disable-feedback-logo{height:31px} .is-loadable-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#e2e4e7;color:transparent;display:inline-block}@media screen and (prefers-reduced-motion: reduce){.is-loadable-placeholder{animation:none}}.is-loadable-placeholder.is-inline{display:inline}.is-loadable-placeholder.is-block{display:block;margin-top:1em}.is-loadable-placeholder.is-block:first-child{margin-top:0}.is-loadable-placeholder.is-block p{margin:0} .transactions .components-notice{margin-left:0;margin-right:0;margin-bottom:1em}.transactions__customer-support,.transactions__customer-statements{max-width:500px;position:relative}.transactions__customer-support .components-base-control__field .components-text-control__input,.transactions__customer-statements .components-base-control__field .components-text-control__input{padding-right:55px}.transactions__customer-support .input-help-text,.transactions__customer-statements .input-help-text{position:absolute;right:10px;top:38px;font-size:12px;color:#757575}@media(min-width: 783px){.transactions__customer-support .input-help-text,.transactions__customer-statements .input-help-text{top:32px}} .iti { position: relative; display: inline-block; } .iti * { box-sizing: border-box; -moz-box-sizing: border-box; } .iti__hide { display: none; } .iti__v-hide { visibility: hidden; } .iti input, .iti input[type=text], .iti input[type=tel] { position: relative; z-index: 0; margin-top: 0 !important; margin-bottom: 0 !important; padding-right: 36px; margin-right: 0; } .iti__flag-container { position: absolute; top: 0; bottom: 0; right: 0; padding: 1px; } .iti__selected-flag { z-index: 1; position: relative; display: flex; align-items: center; height: 100%; padding: 0 6px 0 8px; } .iti__arrow { margin-left: 6px; width: 0; height: 0; border-left: 3px solid transparent; border-right: 3px solid transparent; border-top: 4px solid #555; } .iti__arrow--up { border-top: none; border-bottom: 4px solid #555; } .iti__country-list { position: absolute; z-index: 2; list-style: none; text-align: left; padding: 0; margin: 0 0 0 -1px; box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2); background-color: white; border: 1px solid #CCC; white-space: nowrap; max-height: 200px; overflow-y: scroll; -webkit-overflow-scrolling: touch; } .iti__country-list--dropup { bottom: 100%; margin-bottom: -1px; } @media (max-width: 500px) { .iti__country-list { white-space: normal; } } .iti__flag-box { display: inline-block; width: 20px; } .iti__divider { padding-bottom: 5px; margin-bottom: 5px; border-bottom: 1px solid #CCC; } .iti__country { padding: 5px 10px; outline: none; } .iti__dial-code { color: #999; } .iti__country.iti__highlight { background-color: rgba(0, 0, 0, 0.05); } .iti__flag-box, .iti__country-name, .iti__dial-code { vertical-align: middle; } .iti__flag-box, .iti__country-name { margin-right: 6px; } .iti--allow-dropdown input, .iti--allow-dropdown input[type=text], .iti--allow-dropdown input[type=tel], .iti--separate-dial-code input, .iti--separate-dial-code input[type=text], .iti--separate-dial-code input[type=tel] { padding-right: 6px; padding-left: 52px; margin-left: 0; } .iti--allow-dropdown .iti__flag-container, .iti--separate-dial-code .iti__flag-container { right: auto; left: 0; } .iti--allow-dropdown .iti__flag-container:hover { cursor: pointer; } .iti--allow-dropdown .iti__flag-container:hover .iti__selected-flag { background-color: rgba(0, 0, 0, 0.05); } .iti--allow-dropdown input[disabled] + .iti__flag-container:hover, .iti--allow-dropdown input[readonly] + .iti__flag-container:hover { cursor: default; } .iti--allow-dropdown input[disabled] + .iti__flag-container:hover .iti__selected-flag, .iti--allow-dropdown input[readonly] + .iti__flag-container:hover .iti__selected-flag { background-color: transparent; } .iti--separate-dial-code .iti__selected-flag { background-color: rgba(0, 0, 0, 0.05); } .iti--separate-dial-code .iti__selected-dial-code { margin-left: 6px; } .iti--container { position: absolute; top: -1000px; left: -1000px; z-index: 1060; padding: 1px; } .iti--container:hover { cursor: pointer; } .iti-mobile .iti--container { top: 30px; bottom: 30px; left: 30px; right: 30px; position: fixed; } .iti-mobile .iti__country-list { max-height: 100%; width: 100%; } .iti-mobile .iti__country { padding: 10px 10px; line-height: 1.5em; } .iti__flag { width: 20px; } .iti__flag.iti__be { width: 18px; } .iti__flag.iti__ch { width: 15px; } .iti__flag.iti__mc { width: 19px; } .iti__flag.iti__ne { width: 18px; } .iti__flag.iti__np { width: 13px; } .iti__flag.iti__va { width: 15px; } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .iti__flag { background-size: 5652px 15px; } } .iti__flag.iti__ac { height: 10px; background-position: 0px 0px; } .iti__flag.iti__ad { height: 14px; background-position: -22px 0px; } .iti__flag.iti__ae { height: 10px; background-position: -44px 0px; } .iti__flag.iti__af { height: 14px; background-position: -66px 0px; } .iti__flag.iti__ag { height: 14px; background-position: -88px 0px; } .iti__flag.iti__ai { height: 10px; background-position: -110px 0px; } .iti__flag.iti__al { height: 15px; background-position: -132px 0px; } .iti__flag.iti__am { height: 10px; background-position: -154px 0px; } .iti__flag.iti__ao { height: 14px; background-position: -176px 0px; } .iti__flag.iti__aq { height: 14px; background-position: -198px 0px; } .iti__flag.iti__ar { height: 13px; background-position: -220px 0px; } .iti__flag.iti__as { height: 10px; background-position: -242px 0px; } .iti__flag.iti__at { height: 14px; background-position: -264px 0px; } .iti__flag.iti__au { height: 10px; background-position: -286px 0px; } .iti__flag.iti__aw { height: 14px; background-position: -308px 0px; } .iti__flag.iti__ax { height: 13px; background-position: -330px 0px; } .iti__flag.iti__az { height: 10px; background-position: -352px 0px; } .iti__flag.iti__ba { height: 10px; background-position: -374px 0px; } .iti__flag.iti__bb { height: 14px; background-position: -396px 0px; } .iti__flag.iti__bd { height: 12px; background-position: -418px 0px; } .iti__flag.iti__be { height: 15px; background-position: -440px 0px; } .iti__flag.iti__bf { height: 14px; background-position: -460px 0px; } .iti__flag.iti__bg { height: 12px; background-position: -482px 0px; } .iti__flag.iti__bh { height: 12px; background-position: -504px 0px; } .iti__flag.iti__bi { height: 12px; background-position: -526px 0px; } .iti__flag.iti__bj { height: 14px; background-position: -548px 0px; } .iti__flag.iti__bl { height: 14px; background-position: -570px 0px; } .iti__flag.iti__bm { height: 10px; background-position: -592px 0px; } .iti__flag.iti__bn { height: 10px; background-position: -614px 0px; } .iti__flag.iti__bo { height: 14px; background-position: -636px 0px; } .iti__flag.iti__bq { height: 14px; background-position: -658px 0px; } .iti__flag.iti__br { height: 14px; background-position: -680px 0px; } .iti__flag.iti__bs { height: 10px; background-position: -702px 0px; } .iti__flag.iti__bt { height: 14px; background-position: -724px 0px; } .iti__flag.iti__bv { height: 15px; background-position: -746px 0px; } .iti__flag.iti__bw { height: 14px; background-position: -768px 0px; } .iti__flag.iti__by { height: 10px; background-position: -790px 0px; } .iti__flag.iti__bz { height: 14px; background-position: -812px 0px; } .iti__flag.iti__ca { height: 10px; background-position: -834px 0px; } .iti__flag.iti__cc { height: 10px; background-position: -856px 0px; } .iti__flag.iti__cd { height: 15px; background-position: -878px 0px; } .iti__flag.iti__cf { height: 14px; background-position: -900px 0px; } .iti__flag.iti__cg { height: 14px; background-position: -922px 0px; } .iti__flag.iti__ch { height: 15px; background-position: -944px 0px; } .iti__flag.iti__ci { height: 14px; background-position: -961px 0px; } .iti__flag.iti__ck { height: 10px; background-position: -983px 0px; } .iti__flag.iti__cl { height: 14px; background-position: -1005px 0px; } .iti__flag.iti__cm { height: 14px; background-position: -1027px 0px; } .iti__flag.iti__cn { height: 14px; background-position: -1049px 0px; } .iti__flag.iti__co { height: 14px; background-position: -1071px 0px; } .iti__flag.iti__cp { height: 14px; background-position: -1093px 0px; } .iti__flag.iti__cr { height: 12px; background-position: -1115px 0px; } .iti__flag.iti__cu { height: 10px; background-position: -1137px 0px; } .iti__flag.iti__cv { height: 12px; background-position: -1159px 0px; } .iti__flag.iti__cw { height: 14px; background-position: -1181px 0px; } .iti__flag.iti__cx { height: 10px; background-position: -1203px 0px; } .iti__flag.iti__cy { height: 14px; background-position: -1225px 0px; } .iti__flag.iti__cz { height: 14px; background-position: -1247px 0px; } .iti__flag.iti__de { height: 12px; background-position: -1269px 0px; } .iti__flag.iti__dg { height: 10px; background-position: -1291px 0px; } .iti__flag.iti__dj { height: 14px; background-position: -1313px 0px; } .iti__flag.iti__dk { height: 15px; background-position: -1335px 0px; } .iti__flag.iti__dm { height: 10px; background-position: -1357px 0px; } .iti__flag.iti__do { height: 14px; background-position: -1379px 0px; } .iti__flag.iti__dz { height: 14px; background-position: -1401px 0px; } .iti__flag.iti__ea { height: 14px; background-position: -1423px 0px; } .iti__flag.iti__ec { height: 14px; background-position: -1445px 0px; } .iti__flag.iti__ee { height: 13px; background-position: -1467px 0px; } .iti__flag.iti__eg { height: 14px; background-position: -1489px 0px; } .iti__flag.iti__eh { height: 10px; background-position: -1511px 0px; } .iti__flag.iti__er { height: 10px; background-position: -1533px 0px; } .iti__flag.iti__es { height: 14px; background-position: -1555px 0px; } .iti__flag.iti__et { height: 10px; background-position: -1577px 0px; } .iti__flag.iti__eu { height: 14px; background-position: -1599px 0px; } .iti__flag.iti__fi { height: 12px; background-position: -1621px 0px; } .iti__flag.iti__fj { height: 10px; background-position: -1643px 0px; } .iti__flag.iti__fk { height: 10px; background-position: -1665px 0px; } .iti__flag.iti__fm { height: 11px; background-position: -1687px 0px; } .iti__flag.iti__fo { height: 15px; background-position: -1709px 0px; } .iti__flag.iti__fr { height: 14px; background-position: -1731px 0px; } .iti__flag.iti__ga { height: 15px; background-position: -1753px 0px; } .iti__flag.iti__gb { height: 10px; background-position: -1775px 0px; } .iti__flag.iti__gd { height: 12px; background-position: -1797px 0px; } .iti__flag.iti__ge { height: 14px; background-position: -1819px 0px; } .iti__flag.iti__gf { height: 14px; background-position: -1841px 0px; } .iti__flag.iti__gg { height: 14px; background-position: -1863px 0px; } .iti__flag.iti__gh { height: 14px; background-position: -1885px 0px; } .iti__flag.iti__gi { height: 10px; background-position: -1907px 0px; } .iti__flag.iti__gl { height: 14px; background-position: -1929px 0px; } .iti__flag.iti__gm { height: 14px; background-position: -1951px 0px; } .iti__flag.iti__gn { height: 14px; background-position: -1973px 0px; } .iti__flag.iti__gp { height: 14px; background-position: -1995px 0px; } .iti__flag.iti__gq { height: 14px; background-position: -2017px 0px; } .iti__flag.iti__gr { height: 14px; background-position: -2039px 0px; } .iti__flag.iti__gs { height: 10px; background-position: -2061px 0px; } .iti__flag.iti__gt { height: 13px; background-position: -2083px 0px; } .iti__flag.iti__gu { height: 11px; background-position: -2105px 0px; } .iti__flag.iti__gw { height: 10px; background-position: -2127px 0px; } .iti__flag.iti__gy { height: 12px; background-position: -2149px 0px; } .iti__flag.iti__hk { height: 14px; background-position: -2171px 0px; } .iti__flag.iti__hm { height: 10px; background-position: -2193px 0px; } .iti__flag.iti__hn { height: 10px; background-position: -2215px 0px; } .iti__flag.iti__hr { height: 10px; background-position: -2237px 0px; } .iti__flag.iti__ht { height: 12px; background-position: -2259px 0px; } .iti__flag.iti__hu { height: 10px; background-position: -2281px 0px; } .iti__flag.iti__ic { height: 14px; background-position: -2303px 0px; } .iti__flag.iti__id { height: 14px; background-position: -2325px 0px; } .iti__flag.iti__ie { height: 10px; background-position: -2347px 0px; } .iti__flag.iti__il { height: 15px; background-position: -2369px 0px; } .iti__flag.iti__im { height: 10px; background-position: -2391px 0px; } .iti__flag.iti__in { height: 14px; background-position: -2413px 0px; } .iti__flag.iti__io { height: 10px; background-position: -2435px 0px; } .iti__flag.iti__iq { height: 14px; background-position: -2457px 0px; } .iti__flag.iti__ir { height: 12px; background-position: -2479px 0px; } .iti__flag.iti__is { height: 15px; background-position: -2501px 0px; } .iti__flag.iti__it { height: 14px; background-position: -2523px 0px; } .iti__flag.iti__je { height: 12px; background-position: -2545px 0px; } .iti__flag.iti__jm { height: 10px; background-position: -2567px 0px; } .iti__flag.iti__jo { height: 10px; background-position: -2589px 0px; } .iti__flag.iti__jp { height: 14px; background-position: -2611px 0px; } .iti__flag.iti__ke { height: 14px; background-position: -2633px 0px; } .iti__flag.iti__kg { height: 12px; background-position: -2655px 0px; } .iti__flag.iti__kh { height: 13px; background-position: -2677px 0px; } .iti__flag.iti__ki { height: 10px; background-position: -2699px 0px; } .iti__flag.iti__km { height: 12px; background-position: -2721px 0px; } .iti__flag.iti__kn { height: 14px; background-position: -2743px 0px; } .iti__flag.iti__kp { height: 10px; background-position: -2765px 0px; } .iti__flag.iti__kr { height: 14px; background-position: -2787px 0px; } .iti__flag.iti__kw { height: 10px; background-position: -2809px 0px; } .iti__flag.iti__ky { height: 10px; background-position: -2831px 0px; } .iti__flag.iti__kz { height: 10px; background-position: -2853px 0px; } .iti__flag.iti__la { height: 14px; background-position: -2875px 0px; } .iti__flag.iti__lb { height: 14px; background-position: -2897px 0px; } .iti__flag.iti__lc { height: 10px; background-position: -2919px 0px; } .iti__flag.iti__li { height: 12px; background-position: -2941px 0px; } .iti__flag.iti__lk { height: 10px; background-position: -2963px 0px; } .iti__flag.iti__lr { height: 11px; background-position: -2985px 0px; } .iti__flag.iti__ls { height: 14px; background-position: -3007px 0px; } .iti__flag.iti__lt { height: 12px; background-position: -3029px 0px; } .iti__flag.iti__lu { height: 12px; background-position: -3051px 0px; } .iti__flag.iti__lv { height: 10px; background-position: -3073px 0px; } .iti__flag.iti__ly { height: 10px; background-position: -3095px 0px; } .iti__flag.iti__ma { height: 14px; background-position: -3117px 0px; } .iti__flag.iti__mc { height: 15px; background-position: -3139px 0px; } .iti__flag.iti__md { height: 10px; background-position: -3160px 0px; } .iti__flag.iti__me { height: 10px; background-position: -3182px 0px; } .iti__flag.iti__mf { height: 14px; background-position: -3204px 0px; } .iti__flag.iti__mg { height: 14px; background-position: -3226px 0px; } .iti__flag.iti__mh { height: 11px; background-position: -3248px 0px; } .iti__flag.iti__mk { height: 10px; background-position: -3270px 0px; } .iti__flag.iti__ml { height: 14px; background-position: -3292px 0px; } .iti__flag.iti__mm { height: 14px; background-position: -3314px 0px; } .iti__flag.iti__mn { height: 10px; background-position: -3336px 0px; } .iti__flag.iti__mo { height: 14px; background-position: -3358px 0px; } .iti__flag.iti__mp { height: 10px; background-position: -3380px 0px; } .iti__flag.iti__mq { height: 14px; background-position: -3402px 0px; } .iti__flag.iti__mr { height: 14px; background-position: -3424px 0px; } .iti__flag.iti__ms { height: 10px; background-position: -3446px 0px; } .iti__flag.iti__mt { height: 14px; background-position: -3468px 0px; } .iti__flag.iti__mu { height: 14px; background-position: -3490px 0px; } .iti__flag.iti__mv { height: 14px; background-position: -3512px 0px; } .iti__flag.iti__mw { height: 14px; background-position: -3534px 0px; } .iti__flag.iti__mx { height: 12px; background-position: -3556px 0px; } .iti__flag.iti__my { height: 10px; background-position: -3578px 0px; } .iti__flag.iti__mz { height: 14px; background-position: -3600px 0px; } .iti__flag.iti__na { height: 14px; background-position: -3622px 0px; } .iti__flag.iti__nc { height: 10px; background-position: -3644px 0px; } .iti__flag.iti__ne { height: 15px; background-position: -3666px 0px; } .iti__flag.iti__nf { height: 10px; background-position: -3686px 0px; } .iti__flag.iti__ng { height: 10px; background-position: -3708px 0px; } .iti__flag.iti__ni { height: 12px; background-position: -3730px 0px; } .iti__flag.iti__nl { height: 14px; background-position: -3752px 0px; } .iti__flag.iti__no { height: 15px; background-position: -3774px 0px; } .iti__flag.iti__np { height: 15px; background-position: -3796px 0px; } .iti__flag.iti__nr { height: 10px; background-position: -3811px 0px; } .iti__flag.iti__nu { height: 10px; background-position: -3833px 0px; } .iti__flag.iti__nz { height: 10px; background-position: -3855px 0px; } .iti__flag.iti__om { height: 10px; background-position: -3877px 0px; } .iti__flag.iti__pa { height: 14px; background-position: -3899px 0px; } .iti__flag.iti__pe { height: 14px; background-position: -3921px 0px; } .iti__flag.iti__pf { height: 14px; background-position: -3943px 0px; } .iti__flag.iti__pg { height: 15px; background-position: -3965px 0px; } .iti__flag.iti__ph { height: 10px; background-position: -3987px 0px; } .iti__flag.iti__pk { height: 14px; background-position: -4009px 0px; } .iti__flag.iti__pl { height: 13px; background-position: -4031px 0px; } .iti__flag.iti__pm { height: 14px; background-position: -4053px 0px; } .iti__flag.iti__pn { height: 10px; background-position: -4075px 0px; } .iti__flag.iti__pr { height: 14px; background-position: -4097px 0px; } .iti__flag.iti__ps { height: 10px; background-position: -4119px 0px; } .iti__flag.iti__pt { height: 14px; background-position: -4141px 0px; } .iti__flag.iti__pw { height: 13px; background-position: -4163px 0px; } .iti__flag.iti__py { height: 11px; background-position: -4185px 0px; } .iti__flag.iti__qa { height: 8px; background-position: -4207px 0px; } .iti__flag.iti__re { height: 14px; background-position: -4229px 0px; } .iti__flag.iti__ro { height: 14px; background-position: -4251px 0px; } .iti__flag.iti__rs { height: 14px; background-position: -4273px 0px; } .iti__flag.iti__ru { height: 14px; background-position: -4295px 0px; } .iti__flag.iti__rw { height: 14px; background-position: -4317px 0px; } .iti__flag.iti__sa { height: 14px; background-position: -4339px 0px; } .iti__flag.iti__sb { height: 10px; background-position: -4361px 0px; } .iti__flag.iti__sc { height: 10px; background-position: -4383px 0px; } .iti__flag.iti__sd { height: 10px; background-position: -4405px 0px; } .iti__flag.iti__se { height: 13px; background-position: -4427px 0px; } .iti__flag.iti__sg { height: 14px; background-position: -4449px 0px; } .iti__flag.iti__sh { height: 10px; background-position: -4471px 0px; } .iti__flag.iti__si { height: 10px; background-position: -4493px 0px; } .iti__flag.iti__sj { height: 15px; background-position: -4515px 0px; } .iti__flag.iti__sk { height: 14px; background-position: -4537px 0px; } .iti__flag.iti__sl { height: 14px; background-position: -4559px 0px; } .iti__flag.iti__sm { height: 15px; background-position: -4581px 0px; } .iti__flag.iti__sn { height: 14px; background-position: -4603px 0px; } .iti__flag.iti__so { height: 14px; background-position: -4625px 0px; } .iti__flag.iti__sr { height: 14px; background-position: -4647px 0px; } .iti__flag.iti__ss { height: 10px; background-position: -4669px 0px; } .iti__flag.iti__st { height: 10px; background-position: -4691px 0px; } .iti__flag.iti__sv { height: 12px; background-position: -4713px 0px; } .iti__flag.iti__sx { height: 14px; background-position: -4735px 0px; } .iti__flag.iti__sy { height: 14px; background-position: -4757px 0px; } .iti__flag.iti__sz { height: 14px; background-position: -4779px 0px; } .iti__flag.iti__ta { height: 10px; background-position: -4801px 0px; } .iti__flag.iti__tc { height: 10px; background-position: -4823px 0px; } .iti__flag.iti__td { height: 14px; background-position: -4845px 0px; } .iti__flag.iti__tf { height: 14px; background-position: -4867px 0px; } .iti__flag.iti__tg { height: 13px; background-position: -4889px 0px; } .iti__flag.iti__th { height: 14px; background-position: -4911px 0px; } .iti__flag.iti__tj { height: 10px; background-position: -4933px 0px; } .iti__flag.iti__tk { height: 10px; background-position: -4955px 0px; } .iti__flag.iti__tl { height: 10px; background-position: -4977px 0px; } .iti__flag.iti__tm { height: 14px; background-position: -4999px 0px; } .iti__flag.iti__tn { height: 14px; background-position: -5021px 0px; } .iti__flag.iti__to { height: 10px; background-position: -5043px 0px; } .iti__flag.iti__tr { height: 14px; background-position: -5065px 0px; } .iti__flag.iti__tt { height: 12px; background-position: -5087px 0px; } .iti__flag.iti__tv { height: 10px; background-position: -5109px 0px; } .iti__flag.iti__tw { height: 14px; background-position: -5131px 0px; } .iti__flag.iti__tz { height: 14px; background-position: -5153px 0px; } .iti__flag.iti__ua { height: 14px; background-position: -5175px 0px; } .iti__flag.iti__ug { height: 14px; background-position: -5197px 0px; } .iti__flag.iti__um { height: 11px; background-position: -5219px 0px; } .iti__flag.iti__un { height: 14px; background-position: -5241px 0px; } .iti__flag.iti__us { height: 11px; background-position: -5263px 0px; } .iti__flag.iti__uy { height: 14px; background-position: -5285px 0px; } .iti__flag.iti__uz { height: 10px; background-position: -5307px 0px; } .iti__flag.iti__va { height: 15px; background-position: -5329px 0px; } .iti__flag.iti__vc { height: 14px; background-position: -5346px 0px; } .iti__flag.iti__ve { height: 14px; background-position: -5368px 0px; } .iti__flag.iti__vg { height: 10px; background-position: -5390px 0px; } .iti__flag.iti__vi { height: 14px; background-position: -5412px 0px; } .iti__flag.iti__vn { height: 14px; background-position: -5434px 0px; } .iti__flag.iti__vu { height: 12px; background-position: -5456px 0px; } .iti__flag.iti__wf { height: 14px; background-position: -5478px 0px; } .iti__flag.iti__ws { height: 10px; background-position: -5500px 0px; } .iti__flag.iti__xk { height: 15px; background-position: -5522px 0px; } .iti__flag.iti__ye { height: 14px; background-position: -5544px 0px; } .iti__flag.iti__yt { height: 14px; background-position: -5566px 0px; } .iti__flag.iti__za { height: 14px; background-position: -5588px 0px; } .iti__flag.iti__zm { height: 14px; background-position: -5610px 0px; } .iti__flag.iti__zw { height: 10px; background-position: -5632px 0px; } .iti__flag { height: 15px; box-shadow: 0px 0px 1px 0px #888; background-image: url(007b2705c0a8f69dfdf6.png); background-repeat: no-repeat; background-color: #DBDBDB; background-position: 20px 0; } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .iti__flag { background-image: url(9d5328fb490cddd43f66.png); } } .iti__flag.iti__np { background-color: transparent; } #payment .wc_payment_methods.payment_methods.methods+.woopay-save-new-user-container:not(:empty){margin-top:24px}#payment .wc_payment_methods.payment_methods.methods+.woopay-save-new-user-container:not(:empty)+.place-order{margin-top:24px}.woopay-save-new-user-container h3{margin-bottom:0}.woopay-save-new-user-container .components-checkbox-control__input-container{margin-right:8px;height:auto}.woopay-save-new-user-container .components-checkbox-control__input-container .components-checkbox-control__input[type=checkbox]{height:16px;width:16px}.woopay-save-new-user-container .components-checkbox-control__input-container svg{height:16px;width:16px;left:0;top:0}.woopay-save-new-user-container .components-checkbox-control__input-container label{font-size:14px}.woopay-save-new-user-container .save-details-form.form-row{display:flex;flex-direction:column;font-size:1rem;line-height:1.25rem;font-weight:400;gap:16px;transition:max-height .5s ease-in-out;margin:0 !important;padding:0 !important;overflow-y:hidden;max-height:21.875rem}.woopay-save-new-user-container .save-details-form.form-row::before,.woopay-save-new-user-container .save-details-form.form-row::after{display:none}.woopay-save-new-user-container .save-details-form.form-row>div{margin-top:0}.woopay-save-new-user-container .save-details-form.form-row .additional-information{font-size:var(--wp--preset--font-size--small, 14px);line-height:21px;text-align:left}.woopay-save-new-user-container .save-details-form.form-row .tos{font-size:13px}.woopay-save-new-user-container .save-details-form.form-row #validate-error-invalid-woopay-phone-number{font-size:13px;line-height:1;margin-bottom:0;color:#cc1818}.woopay-save-new-user-container .save-details-form.form-row .line{border:.5px solid #dcdcde}.woopay-save-new-user-container .save-details-form.form-row .phone-input{font-size:1rem;background-color:#fff;box-shadow:none;border:1px solid #ddd;border-radius:5px;width:calc(100% - .25rem)}.woopay-save-new-user-container .save-details-form.form-row .phone-input::placeholder{font-weight:400;color:#a7aaad}.woopay-save-new-user-container .save-details-form.form-row .phone-input.has-error{outline-color:#cc1818}.woopay-save-new-user-container .iti{margin-top:16px}.wc-block-components-form .woopay-save-new-user-container .wc-block-components-text-input:only-child{margin-top:0}#phone-number{height:42px;box-shadow:inset 0 1px #d6d6d6;border:none;font-size:15px;line-height:26px}#payment .save-details-form.place-order{margin-top:8px}.iti__flag{background-image:url(007b2705c0a8f69dfdf6.png)}@media(-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.iti__flag{background-image:url(9d5328fb490cddd43f66.png)}}.iti{width:100%}.iti--container{margin-top:0}.iti__selected-flag{background-color:inherit !important;padding:0 6px 0 12px}.iti__selected-flag .iti__flag{transform:scale(1.1)}.iti__selected-flag .iti__selected-dial-code{font-size:16px;color:#2f2f2f}.iti__selected-flag .iti__arrow{border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #2c3338}.iti__selected-flag .iti__arrow--up{border-top:none;border-bottom:5px solid #2c3338}.wp-admin .iti .iti__arrow{border:0;width:10px;height:18px;background:url("data:image/svg+xml;utf8,<svg viewBox=\"4 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z\"></path></svg>")}.wp-admin .iti .iti__arrow--up{transform:scaleY(-1)}.wp-admin .iti .iti__country-list{border:1px solid #949494;border-radius:2px;box-shadow:none}.wp-admin .iti .iti__selected-dial-code,.wp-admin .iti .phone-input.input-text,.wp-admin .iti .iti__country{font-size:inherit;color:#2c3338}@media(max-width: 782px){.wp-admin .iti .iti__selected-dial-code,.wp-admin .iti .phone-input.input-text,.wp-admin .iti .iti__country{font-size:16px}}.wp-admin.iti-mobile .iti--container{top:76px;width:auto} .wcpay-settings__account-business-support-phone-input p{margin-bottom:0}.wcpay-settings__account-business-support-phone-input .iti{margin-top:0}.wcpay-settings__account-business-support-phone-input input{height:40px;padding-right:12px;width:100%;border-radius:0}#account-business-support-phone-input{height:40px;padding-right:12px;width:100%;border-radius:0} .deposits .schedule-controls{display:flex;flex-wrap:wrap;margin-bottom:0}.deposits .schedule-controls .components-base-control{margin-bottom:0}.deposits .schedule-controls .components-select-control{padding-right:16px;margin-bottom:0}.deposits .help-text{color:#757575;margin-top:8px;font-size:12px;margin-bottom:1em} .payment-methods{position:relative}.payment-methods .wcpay-card-body{padding:0}.payment-methods .payment-methods-list{margin:0}.payment-methods .wcpay-tooltip__tooltip.wcpay-tooltip__tooltip--dark a{color:#fff;text-decoration:underline}.payment-methods .components-popover__content{position:relative;right:120px}.payment-methods__header{justify-content:space-between}.payment-methods__heading{margin:0;font-size:16px;display:flex;align-items:center;flex-wrap:wrap;line-height:2em}.payment-methods__heading>*:not(:last-child){margin-right:4px}.payment-methods__heading .wcpay-pill{padding:2px 8px}.payment-methods__express-checkouts{background:url(../assets/images/payment-methods/all_local_payments.svg?ver=3ddc2729072d70f2a6ce) no-repeat right;background-size:auto;margin-right:10px}.payment-methods__express-checkouts.background-local-payment-methods{background:url(../assets/images/payment-methods/local_payments.svg?ver=c49f8de3c719563325d9) no-repeat right}@media(max-width: 1024px){.payment-methods__express-checkouts.background-local-payment-methods{background-image:none}.payment-methods__express-checkouts.background-local-payment-methods h3,.payment-methods__express-checkouts.background-local-payment-methods p{padding-right:0}}.payment-methods__express-checkouts h3{font-size:14px;padding-right:215px}.payment-methods__express-checkouts p{padding-right:230px}.payment-methods__express-checkouts-get-started{margin-right:16px}.payment-methods__express-checkouts-actions{padding-top:11px}@media(max-width: 1024px){.payment-methods__express-checkouts{background-image:none}.payment-methods__express-checkouts h3,.payment-methods__express-checkouts p{padding-right:0}}.payment-methods .payment-methods-header{justify-content:flex-end}.payment-methods.is-loading::after{content:" ";position:absolute;top:0;bottom:0;width:100%;height:100%;animation:loading-fade 1.6s ease-in-out infinite;background-color:rgba(226,228,231,.65)}@media screen and (prefers-reduced-motion: reduce){.payment-methods.is-loading::after{animation:none}}.payment-methods .components-popover{position:fixed}.rtl .iti__country-list{display:none}.rtl .iti__arrow{display:none} .wcpay-tooltip__content-wrapper{display:contents}.wcpay-tooltip__content-wrapper>div{display:inline}.wcpay-tooltip__content-wrapper [role=button]{cursor:pointer;display:inline-flex;align-items:center;color:#949494;transition:all .3s ease;fill:currentColor;margin:0 .4em;vertical-align:text-bottom}.wcpay-tooltip__content-wrapper [role=button]:focus,.wcpay-tooltip__content-wrapper [role=button]:hover,.wcpay-tooltip__content-wrapper [role=button]:active{outline:none;color:var(--wp-admin-theme-color, #007cba)}.wcpay-tooltip__tooltip-wrapper{visibility:hidden;position:fixed;opacity:0;transition:opacity 150ms ease-in;z-index:100010;left:0;white-space:normal;font-size:12px;font-weight:400}.wcpay-tooltip__tooltip-wrapper.is-hiding{opacity:0 !important;transition:opacity 200ms ease-out 300ms}.wcpay-tooltip__tooltip{position:relative;color:#fff;background-color:#1e1e1e;padding:10px;text-align:center}.wcpay-tooltip__tooltip a{color:var(--wp-admin-theme-color-background-25, #c5d9ed);text-decoration:underline}.wcpay-tooltip__tooltip ul{list-style:inherit;margin-left:20px}.wcpay-tooltip--click__content-wrapper{cursor:pointer}.wcpay-tooltip--click__tooltip{position:relative;padding:12px;color:#1e1e1e;text-align:left;border-radius:2px;border:1px solid #ccc;background-color:#fff;box-shadow:0 2px 6px 0 rgba(0,0,0,.05)}.wcpay-tooltip--click__tooltip a{color:var(--wp-admin-theme-color, #007cba)}.wcpay-tooltip--click__tooltip::after{display:none} .wcpay-tooltip__tooltip-wrapper .wcpay-fees-tooltip{display:flex;flex-direction:column;gap:4px;padding:5px}.wcpay-tooltip__tooltip-wrapper .wcpay-fees-tooltip>div{display:flex;text-align:left;gap:16px;align-items:center;justify-content:space-between}.wcpay-tooltip__tooltip-wrapper .wcpay-fees-tooltip>div>div{white-space:nowrap}.wcpay-tooltip__tooltip-wrapper .wcpay-fees-tooltip__bold{font-weight:600}.wcpay-tooltip__tooltip-wrapper .wcpay-fees-tooltip__hint-text{font-size:95%;color:#ccc;margin-top:8px} .chip{font-size:13px;font-size:0.8125rem;display:inline-flex;align-items:center;justify-content:center;font-weight:300;padding:.25rem .5rem;border-radius:.2rem;line-height:1.5;white-space:nowrap}.chip.chip-has-icon{padding-inline-start:4px;gap:2px}.chip.chip-primary{background:#f0f6fc;color:#0a4b78}.chip.chip-primary svg{fill:#0a4b78}.chip.chip-success{background:#edfaef;color:#005c12}.chip.chip-success svg{fill:#005c12}.chip.chip-light{background:#f6f7f7;color:#2c3338}.chip.chip-light svg{fill:#2c3338}.chip.chip-alert{background:#fcf0f1;color:#8a2424}.chip.chip-alert svg{fill:#8a2424}.chip.chip-warning{background:#fcf9e8;color:#614200}.chip.chip-warning svg{fill:#614200} .wcpay-promotional-badge{display:inline-flex;align-items:center;gap:4px}.wcpay-promotional-badge .wcpay-tooltip__content-wrapper{display:inline-flex;align-items:center;background:none;border:none;padding:0;margin:0;cursor:pointer}.wcpay-promotional-badge .wcpay-tooltip__content-wrapper>div{margin-right:0} .woocommerce-pill.wcpay-pill{border-color:#757575;color:#757575}.woocommerce-pill.wcpay-pill__light{border:0;background:#f0f0f1;color:#2c3338}.woocommerce-pill.wcpay-pill__alert{border:0;background:#fcf9e8;color:#614200}.woocommerce-pill.wcpay-pill__danger{border:0;background:#fcf0f1;color:#8a2424}.woocommerce-pill.wcpay-pill__success{border:0;background:#edfaef;color:#005c12}.woocommerce-pill.wcpay-pill__primary{border:0;background:#f0f6fc;color:#193a6f} .payment-method__list-item{display:flex;flex-direction:column;margin:0;padding:24px}.payment-method__list-item:not(:last-child){box-shadow:inset 0 -1px 0 #e8eaeb}.payment-method{display:flex;background:#fff;justify-content:space-between;align-items:center}@media(min-width: 661px){.payment-method{flex-wrap:nowrap}}.payment-method__text{flex:1 1 100%;order:1;display:flex;justify-content:space-between;flex-direction:column}@media(min-width: 661px){.payment-method__text{flex-wrap:nowrap;flex-direction:row;align-items:center;flex:1 1 auto;order:3}}.payment-method__checkbox{display:flex;align-items:center;justify-content:center}.payment-method__checkbox .components-base-control__field{margin:0 4px 0 0}.payment-method__checkbox .components-checkbox-control__label{display:none}.payment-method__checkbox input[type=checkbox]:disabled{cursor:not-allowed}.payment-method__checkbox input[type=checkbox]:checked:disabled{opacity:.3}@media(max-width: 660px){.payment-method__checkbox{align-self:flex-start;margin-top:5px}}.payment-method__text-container{display:flex;flex:1}@media(max-width: 660px){.payment-method__text-container{flex-wrap:wrap}}.payment-method__label{font-size:14px;font-weight:600;line-height:20px;color:#1e1e1e;margin-bottom:4px}.payment-method__label-desktop{display:block}@media(max-width: 660px){.payment-method__label-desktop{display:none}}.payment-method__label-mobile{display:none}@media(max-width: 660px){.payment-method__label-mobile{display:block;align-self:center;font-size:16px}}.payment-method__required-label{font-size:12px;font-weight:normal;color:#757575;margin-left:8px}.payment-method__description{color:#646970;font-size:13px;line-height:16px;margin-bottom:10px}@media(min-width: 661px){.payment-method__description{margin-bottom:0}}.payment-method__actions{align-items:center;display:flex;justify-content:center;order:3}@media(min-width: 661px){.payment-method__actions{order:4}}.payment-method__action.delete{margin-right:25px}.payment-method__action.delete svg{fill:#a2aab2}.payment-method__icon{flex:0 0 63.69px;margin:1px 17px 1px 1px;align-self:center}@media(min-width: 661px){.payment-method__icon{display:flex;order:2}}@media(max-width: 660px){.payment-method__icon{margin:0 10px 5px 0;flex:0;height:auto}.payment-method__icon img{height:30px}}.payment-method__fees{display:flex;white-space:nowrap}@media(min-width: 661px){.payment-method__fees{justify-content:flex-end;margin-left:16px;flex-shrink:0}}.payment-method .chip{margin-left:8px;padding:2px 8px}@media(max-width: 660px){.payment-method .chip{margin-left:2}}.payment-method__supported-cards{margin-top:16px}@media(max-width: 660px){.payment-method__supported-cards{margin-top:10px;margin-bottom:10px}}.payment-method__supported-cards .payment-methods--logos>div{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.payment-method__supported-cards .payment-methods--logos>div img{width:32px;height:20px;object-fit:contain;margin-right:0} .payment-methods--logos>div{display:flex;align-items:center;flex-wrap:nowrap;min-width:0;max-width:100%;overflow:hidden}.payment-methods--logos>div img{width:37px;height:24px;margin-right:4px;flex-shrink:1;min-width:0;max-width:100%}.payment-methods--logos-count{width:38px;height:24px;background-color:rgba(117,117,117,.1);color:var(--wp--preset--color--contrast, #1e1e1e);text-align:center;line-height:24px;border-radius:3px;font-size:11px;font-weight:600;white-space:nowrap;flex-shrink:1;text-overflow:ellipsis;overflow:hidden}.logo-popover{background-color:#fff;border:1px solid #ddd;border-radius:3px;padding:8px;box-sizing:border-box;box-shadow:0 0 10px 0 rgba(0,0,0,.1);display:grid;gap:8px;justify-content:center;cursor:pointer;width:fit-content}.logo-popover>img{width:38px;height:24px;box-shadow:0 0 0 1px rgba(0,0,0,.1)}.payment-methods--logos-popover>img{object-position:center !important} .payment-confirm-illustration__wrapper{margin:80px auto 24px auto;width:35%}.payment-confirm-illustration__illustrations{position:relative;margin:0 auto;width:122px}.payment-confirm-illustration__illustrations::before{content:" ";position:absolute;top:0;left:0;bottom:0;right:0;background:no-repeat url(../assets/images/cards/unknown.svg?ver=9b703f2bcf3ded532116);transform:translate(-22%, -50%);width:135px;height:105px}.payment-confirm-illustration__payment-icon{position:relative;width:100%;height:auto;border-radius:5%}.payment-confirm-illustration__payment-icon.has-border{background:#fff;border:1px solid #ddd}.payment-confirm-illustration__payment-question-mark-icon{position:absolute;top:-24px;right:-24px;width:45px;height:auto;clip-path:circle(19px);fill:#f0b849;background:#fff;border-radius:999px} .payment-method-requirements-list{list-style-type:disc;list-style-position:inside} .notification-settings .components-notice{margin-left:0;margin-right:0;margin-bottom:1em}.notification-settings .settings__notifications-email-input{max-width:500px} .fraud-protection-level-modal .components-modal__header{border-bottom:1px solid #ddd}.fraud-protection-level-modal .components-modal__body--fraud-protection{padding-top:24px}.fraud-protection-level-modal .components-modal__body--fraud-protection ul{list-style-type:disc;margin-left:24px}.fraud-protection-level-modal .component-modal__text--blocked{color:#b32d2e}.fraud-protection-level-modal .component-modal__text--review{color:#996800}.fraud-protection-level-modal .component-modal__button--confirm{display:block;margin-left:auto}.fraud-protection-level-modal .component-notice--is-info{margin-left:0;margin-right:0;margin-bottom:1em;background-color:#f0f6fc;border-left:none}.fraud-protection-level-modal .component-notice__icon{margin-right:10px;align-self:center}.fraud-protection-level-modal .component-notice__content--flex{display:flex;flex-direction:row;align-items:flex-start}.tour-kit-frame__arrow::before{background:#fff} .fraud-protection-radio-wrapper{display:flex;align-items:center;height:20px;margin-bottom:4px}.fraud-protection-radio-wrapper__item{display:inline-block;margin:0 5px 0 0}.fraud-protection__advanced-level-container{align-items:center;display:flex;flex-direction:row}.fraud-protection__advanced-level-container label{flex:1}.fraud-protection__advanced-level-container p.fraud-protection__text--help-text{margin-bottom:0}.fraud-protection ul{margin-top:0}.fraud-protection ul>li{margin-bottom:0}.fraud-protection ul>li+li{padding-top:16px;border:0;border-top:1px solid #f6f7f7}.fraud-protection ul>li:not(:last-child){padding-bottom:16px;border:0;border-bottom:1px solid #dcdcde}.fraud-protection input[type=radio]{margin:0 10px 0 0;width:20px;height:20px;border-color:#757575}.fraud-protection input[type=radio]:checked{background-color:#fff;border-color:#757575}.fraud-protection input[type=radio]:checked::before{content:"";border-radius:50%;width:12px;height:12px;margin:3px;background-color:var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9));line-height:1.14285714}.fraud-protection__text--help-text{font-size:12px;color:#757575;margin:0 0 0 30px;display:inline-block}.fraud-protection-rule-card{margin-bottom:24px}.fraud-protection__help-icon{fill:var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba));cursor:pointer} .task-collapsible-body{max-height:0;overflow:hidden;opacity:0;transition:max-height 500ms ease-out,opacity 500ms ease-out}.task-collapsible-body.is-active{overflow:visible;max-height:1000px;opacity:1;transition:max-height 500ms ease-in,opacity 500ms ease-in} .wcpay-wizard-task{margin-bottom:0}.wcpay-wizard-task:first-child .wcpay-wizard-task__top-border{display:none}.wcpay-wizard-task:last-child .wcpay-wizard-task__body{border-left:none}.wcpay-wizard-task__top-border,.wcpay-wizard-task__body{min-height:16px;border-left:1px solid #e0e0e0;margin:0 11px}.wcpay-wizard-task__headline{display:flex;align-items:center;flex-direction:row;margin:4px 0;font-weight:600;font-size:16px;color:#2c3338}.is-active .wcpay-wizard-task__headline{color:var(--wp-admin-theme-color)}.wcpay-wizard-task__title{padding:0 16px;display:flex;align-items:center;flex-wrap:wrap}.wcpay-wizard-task__title>*:not(:last-child){margin-right:4px}.wcpay-wizard-task__icon-wrapper{position:relative;width:24px;height:24px;padding:0;border-radius:9999px;border:1px solid #e0e0e0;display:flex;justify-content:center;align-items:center;flex-shrink:0}.is-active .wcpay-wizard-task__icon-wrapper{border-color:var(--wp-admin-theme-color)}.is-completed .wcpay-wizard-task__icon-wrapper{border-color:var(--wp-admin-theme-color);background-color:var(--wp-admin-theme-color)}.wcpay-wizard-task__icon-text{font-size:13px}.is-completed .wcpay-wizard-task__icon-text{opacity:0}.wcpay-wizard-task__icon-checkmark{opacity:0;transition:opacity 500ms ease;position:absolute;top:0;right:0;width:100%;height:100%;fill:#fff}.is-completed .wcpay-wizard-task__icon-checkmark{opacity:1}.wcpay-wizard-task__body{display:flex;flex-direction:column;padding:0 30px;font-size:13px}.wcpay-wizard-task__description-element{margin-top:0;margin-bottom:1em}.wcpay-wizard-task__description-element.is-muted-color{color:#757575}.wcpay-wizard-task__description-element .components-external-link svg{width:1em;height:1em}.wcpay-wizard-task__visible-description-element{position:absolute;margin-left:40px;margin-top:0;margin-bottom:1em}.wcpay-wizard-task__visible-description-element.is-muted-color{color:#757575}.wcpay-wizard-task__visible-description-element .components-external-link svg{width:1em;height:1em}.wcpay-wizard-task .add-payment-methods-task__payment-selector-wrapper{margin-bottom:24px}.wcpay-wizard-task .add-payment-methods-task__payment-selector-title{font-weight:600;font-size:16px}.wcpay-wizard-task .add-payment-methods-task__payment-selector-description{margin-bottom:24px} .components-notice.vat-number-error{margin:0;margin-bottom:15px}.wcpay-vat-number-task__checkbox{margin-bottom:16px}.wcpay-vat-number-task__text-control{margin-bottom:16px}.wcpay-company-data-task__text-control{margin-bottom:16px}.wcpay-company-data-task__textarea-control{margin-bottom:16px} .wcpay-spotlight{position:fixed;bottom:24px;right:24px;z-index:999;width:300px;opacity:0;transform:translateX(100%);transition:opacity .3s ease-out,transform .3s ease-out;font-family:"SF Pro Display",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}.wcpay-spotlight--visible{opacity:1;transform:translateX(0)}@media screen and (max-width: 782px){.wcpay-spotlight{right:0;left:0;bottom:0;width:100%}.wcpay-spotlight .wcpay-spotlight__card{margin:0}.wcpay-spotlight .wcpay-spotlight__card.components-card{border-radius:0}}@media(prefers-reduced-motion: reduce){.wcpay-spotlight{transition:none}}.wcpay-spotlight__container{width:100%}.wcpay-spotlight__card{margin:0;position:relative;overflow:hidden}.wcpay-spotlight__card.components-card{border-radius:4px;box-shadow:0 2px 3px rgba(0,0,0,.05),0 4px 5px rgba(0,0,0,.04),0 4px 5px rgba(0,0,0,.03),0 16px 16px rgba(0,0,0,.02)}.wcpay-spotlight__card.has-image .wcpay-spotlight__header,.wcpay-spotlight__card.has-icon .wcpay-spotlight__header{position:absolute;top:0;right:0;width:100%;z-index:1;background:transparent}.wcpay-spotlight__card.has-image .wcpay-spotlight__image,.wcpay-spotlight__card.has-icon .wcpay-spotlight__image{padding:0}.wcpay-spotlight__header{padding:12px 12px 0}.wcpay-spotlight__header.components-card__header{border-radius:4px 4px 0 0}.wcpay-spotlight__header .wcpay-spotlight__badge,.wcpay-spotlight__header .wcpay-spotlight__heading{margin:0}.wcpay-spotlight__controls{width:100%}.wcpay-spotlight__close-btn{min-width:auto;width:32px;height:32px;padding:4px}.wcpay-spotlight__close-btn:hover{background:transparent}.wcpay-spotlight__close-btn:focus-visible{outline:2px solid #007cba;outline-offset:2px;box-shadow:none}.wcpay-spotlight__close-btn .components-icon{color:#1e1e1e}.wcpay-spotlight__image{padding:0;margin:0}.wcpay-spotlight__image img,.wcpay-spotlight__image svg{width:100%;height:auto;display:block}.wcpay-spotlight__icon{margin-bottom:8px;display:flex;align-items:flex-start}.wcpay-spotlight__icon svg{width:44px;height:44px;padding:6px;background:#fff;flex-shrink:0}.wcpay-spotlight__body{padding:16px}.wcpay-spotlight__card:not(.has-image):not(.has-icon) .wcpay-spotlight__body{padding-top:0}.wcpay-spotlight__card.has-icon .wcpay-spotlight__body{padding:16px}.wcpay-spotlight__badge{margin-bottom:12px}.wcpay-spotlight__heading{font-size:var(--l, 15px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);margin:0 0 12px;color:#1e1e1e}.wcpay-spotlight__description{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--s, 20px);color:#1e1e1e;margin:0}.wcpay-spotlight__description a{color:#2271b1;text-decoration:none}.wcpay-spotlight__description a:hover{text-decoration:underline}.wcpay-spotlight__footnote{font-size:var(--s, 12px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--xs, 16px);color:#1e1e1e;margin:12px 0 0}.wcpay-spotlight__footer{padding:0 16px 24px}.wcpay-spotlight__footer.components-card__footer{border-radius:0 0 4px 4px}.wcpay-spotlight__primary-btn:focus-visible,.wcpay-spotlight__secondary-btn:focus-visible{outline:2px solid #007cba;outline-offset:2px;box-shadow:none}body.woocommerce_page_wc-settings .wcpay-spotlight .wcpay-spotlight__card.components-surface.components-card{border-radius:4px;box-shadow:0 2px 3px rgba(0,0,0,.05),0 4px 5px rgba(0,0,0,.04),0 4px 5px rgba(0,0,0,.03),0 16px 16px rgba(0,0,0,.02)} body.woocommerce_page_wc-settings.woocommerce-payments-checkout-section{background:#fff}body.woocommerce_page_wc-settings.woocommerce-payments-checkout-section .settings-content,body.woocommerce_page_wc-settings.woocommerce-payments-checkout-section #wpbody-content,body.woocommerce_page_wc-settings.woocommerce-payments-checkout-section #wpcontent,body.woocommerce_page_wc-settings.woocommerce-payments-checkout-section #mainform{background:#fff} .express-checkout-settings__breadcrumbs{margin-bottom:40px}.express-checkout-settings__option-muted-text{color:#757575}.express-checkout-settings__cta-selection .components-base-control__label{display:none}.express-checkout-settings__preview{border:1px dashed #ddd;box-sizing:border-box;border-radius:3px;background-color:#f0f0f0;padding:16px}.express-checkout-settings__icons{display:flex;align-items:flex-start}.express-checkout-settings__icon{display:flex;width:64px;height:40px;margin:1px 17px 1px 1px}.express-checkout-settings__icon svg{display:inline-block}.components-radio-control__input[type=radio]:checked::before{box-sizing:border-box}.woopay-settings__badge{background-color:#dcdcde;padding:0 8px;border-radius:2px;font-size:12px;line-height:20px;margin-left:4px}.woopay-settings__global-theme-label{display:inline-flex;align-items:center}.woopay-settings__update-store-logo{display:flex;justify-content:space-between}.woopay-settings__update-store-logo .components-base-control__field{margin-bottom:0}.woopay-settings__update-store-logo .components-button.is-link{color:#007cba;text-decoration:none}.woopay-settings__update-store-logo .delete-uploaded-file-button{color:#757575}.woopay-settings__update-store-logo.has-file{border:1px solid #f0f0f0;padding:8px}.woopay-settings__update-store-logo.has-file .components-form-file-upload{display:none}.woopay-settings__update-store-logo img{margin-top:0 !important}.woopay-settings__appearance-card-settings,.woopay-settings__appearance-card-preview{background-color:#fff;box-shadow:0 0 0 1px #0000001a}.woopay-settings__appearance-card-preview{margin-top:1rem}.woopay-settings__preview{position:relative}.woopay-settings__preview-fade{position:absolute;bottom:0;height:45px;width:100%;background:linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #fff 100%)}.woopay-settings__preview .preview-layout{border:1px solid #ddd;border-radius:4px;box-shadow:0 2px 6px 0 rgba(0,0,0,.05);padding:0;position:relative}.woopay-settings__preview .preview-layout__v-spacer{background-color:#0000;width:100%}.woopay-settings__preview .preview-layout__preview-button{position:absolute;top:.3125rem;right:.3125rem;padding:.125rem .5rem;border-radius:.125rem;background-color:#3858e9;color:#fff;line-height:1rem;font-size:.75rem;font-weight:400;z-index:1}.woopay-settings__preview .preview-layout__store-header{width:100%;display:flex;justify-content:center;position:relative}.woopay-settings__preview .preview-layout__store-header .header-text{color:#50575e;font-size:1.125rem;letter-spacing:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media screen and (max-width: 480px){.woopay-settings__preview .preview-layout__store-header .header-text{padding-left:calc(5.66% + 1.5rem)}}.woopay-settings__preview .preview-layout__chevron-left{position:absolute;left:5.66%;top:0}.woopay-settings__preview .preview-layout__container{width:100%}.woopay-settings__preview .preview-layout__body{width:100%}.woopay-settings__preview .preview-layout__hr{color:#dcdcde;margin:0;border-bottom:0}.woopay-settings__preview .preview-layout__left-column{width:59.08%}.woopay-settings__preview .preview-layout__contact-section{border:.03125rem solid #11111121;border-radius:.25rem}.woopay-settings__preview .preview-layout__contact-field{padding:.5rem}.woopay-settings__preview .preview-layout__contact-field:not(:last-child){border-bottom:.03125rem solid #11111121}.woopay-settings__preview .preview-layout__right-column{width:35.59%}.woopay-settings__preview .preview-layout__columns-container{padding:0 3.797% 0 3.797%;display:flex;justify-content:space-between}.woopay-settings__preview .preview-layout__section-header{font-weight:500;font-size:.5rem;color:#2c3338;line-height:.74669rem;letter-spacing:.01rem;display:flex;justify-content:space-between}.woopay-settings__preview .preview-layout__loading-box{background-color:#f6f7f7;border-radius:.25rem;width:100%}.woopay-settings__preview .preview-layout__text-box{width:100%;font-size:.625rem;color:#646970;text-align:center;font-weight:400;line-height:120%;overflow:hidden}.woopay-settings__preview .preview-layout__shortcode-link{color:#6108ce}.woopay-settings__preview .preview-layout__checkout-button{background-color:#873eff;border-radius:.25rem;align-items:center;display:flex;justify-content:center;color:#fff;font-size:.625em;font-weight:600;line-height:.56rem;letter-spacing:.0125rem}.wcpay-express-checkout__enable .payment-request-settings__location{margin:0 0 0 24px;display:flex;flex-direction:column;align-items:flex-start;gap:8px}.wcpay-express-checkout__enable .wcpay-payment-request-settings__enable,.wcpay-express-checkout__enable .wcpay-woopay-settings__enable{display:flex;flex-direction:column;align-items:flex-start;gap:12px}.wcpay-express-checkout__enable .wcpay-payment-request-settings__enable .components-radio-control:not(:last-child),.wcpay-express-checkout__enable .wcpay-payment-request-settings__enable .components-base-control:not(:last-child),.wcpay-express-checkout__enable .wcpay-woopay-settings__enable .components-radio-control:not(:last-child),.wcpay-express-checkout__enable .wcpay-woopay-settings__enable .components-base-control:not(:last-child){margin-bottom:0}.woopay-settings--appearance{background-color:#0000 !important;box-shadow:none !important}.payment-method-settings__option-help-text{color:#757575;margin-bottom:1em}.payment-method-settings__preview{padding:24px;border:1px solid rgba(25,23,17,.3);border-radius:4px;display:flex;justify-content:center;flex-direction:column;gap:12px}@media screen and (max-width: 782px){.payment-method-settings__preview{flex-wrap:wrap}.payment-method-settings__preview>*{display:block;margin-top:14px}.payment-method-settings__preview>*:first-child{margin-top:0}}.payment-method-settings__preview[data-theme=light]{background:#2b2b2b}.payment-method-settings__preview[data-theme=light-outline]{background:#f0f0f0}.payment-method-settings__preview>div{margin:4px}.payment-method-settings__border-radius{display:flex;gap:16px;margin-block-end:0}.payment-method-settings__border-radius .components-base-control{margin-bottom:0 !important}.payment-method-settings__border-radius__slider{flex-grow:1}.payment-method-settings__border-radius__number-control__suffix{padding-inline:8px}.components-notice__content{display:flex} dist/index.css 0000777 00000420221 15251156605 0007346 0 ustar 00 @media screen and (max-width: 782px){.woocommerce-layout .woocommerce-layout__main{padding-top:16px;padding-bottom:16px}}.woocommerce-payments-page .components-card__body>*:first-child{margin-top:0}.woocommerce-payments-page .components-card__body>*:last-child{margin-bottom:0}.woocommerce-payments-page .woocommerce-table .woocommerce-table__item{white-space:nowrap}.woocommerce-payments-page .woocommerce-table .info-button{width:40px;padding-right:0}.woocommerce-payments-page .woocommerce-table .info-button>a{height:18px;fill:#8c8f94}.woocommerce-payments-page .woocommerce-table .info-button+.woocommerce-table__header{border-left:none}.woocommerce-payments-page .woocommerce-table .info-button .is-placeholder{display:none}.woocommerce-payments-page .woocommerce-table .payment-method__brand{display:block;margin:0 auto;text-align:center}.wcpay-test-mode-notice.components-notice.is-warning{margin:24px 0;padding:12px;background-color:#f5e6b3}.wcpay-test-mode-notice.components-notice.is-informational{margin:24px 0;padding:12px}.wcpay-connect-error-notice.components-notice{margin:24px 0;padding:12px}.payment-method-details{display:flex;align-items:flex-start}@media screen and (max-width: 1023px){.payment-method-details{display:block}}.payment-method-details__column{flex:0 0 50%}.payment-method-details-list-item{display:inline-flex}.payment-method-detail{display:flex;align-items:flex-start;font-size:14px;line-height:28px}.payment-method-detail__label{margin:0;color:#50575e;flex:0 0 36%;font-weight:normal;line-height:20px;padding:4px 0}@media screen and (max-width: 470px){.payment-method-detail__label{flex:0 0 40%}}.payment-method-detail__value{margin:0;color:#2c3338;line-height:20px;padding:4px 0;word-break:break-word}.components-modal__content .components-modal__header .components-button .components-tooltip{display:none}img.payment-method__icon{border:1px solid rgba(0,0,0,.25);border-radius:4px;display:block}img.payment-method__icon.no-border{border:none;border-radius:0}@media screen and (min-width: 783px){.woocommerce-filters-deposits .woocommerce-filters-advanced__fieldset,.woocommerce-filters-disputes .woocommerce-filters-advanced__fieldset,.woocommerce-filters-documents .woocommerce-filters-advanced__fieldset,.woocommerce-filters-transactions .woocommerce-filters-advanced__fieldset{display:grid;grid-template-columns:100px auto 1fr}.woocommerce-filters-deposits .components-select-control .components-input-control__container .components-select-control__input,.woocommerce-filters-disputes .components-select-control .components-input-control__container .components-select-control__input,.woocommerce-filters-documents .components-select-control .components-input-control__container .components-select-control__input,.woocommerce-filters-transactions .components-select-control .components-input-control__container .components-select-control__input{padding-right:var(--main-gap)}} .wcpay-inline-notice.components-notice{margin:24px 0;padding:11px 0 11px 17px;border-left:none;border-radius:2px;justify-content:flex-start}.components-modal__header+.wcpay-inline-notice.components-notice,.wcpay-inline-notice.components-notice:first-child{margin-top:0}.wcpay-inline-notice.components-notice:last-child{margin-bottom:0}.wcpay-inline-notice.components-notice .wcpay-inline-notice__icon{display:flex;align-items:center;align-self:flex-start;min-height:auto;min-width:auto;margin-right:5px}.wcpay-inline-notice.components-notice .wcpay-inline-notice__icon svg{width:22px;height:22px}.wcpay-inline-notice.components-notice .components-notice__content{margin-top:2px;margin-bottom:2px}.wcpay-inline-notice.components-notice .components-notice__content a{white-space:nowrap}.wcpay-inline-notice.components-notice .wcpay-inline-notice__content__actions{padding-top:12px}.wcpay-inline-notice.components-notice a.wcpay-inline-notice__action{text-decoration:none}.wcpay-inline-notice.components-notice.is-dismissible{padding-right:12px}.wcpay-inline-notice.components-notice .components-notice__dismiss{height:24px;width:24px}.wcpay-inline-notice.components-notice .components-notice__dismiss svg{width:15px;height:15px;fill:#1e1e1e}.wcpay-inline-notice.components-notice.is-info{background-color:#f0f6fc}.wcpay-inline-notice.components-notice.is-info .wcpay-inline-notice__icon svg{fill:#0a4b78}.wcpay-inline-notice.components-notice.is-info button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #0a4b78}.wcpay-inline-notice.components-notice.is-info .wcpay-inline-notice__action{color:#0a4b78}.wcpay-inline-notice.components-notice.is-warning{background-color:#fcf9e8}.wcpay-inline-notice.components-notice.is-warning .wcpay-inline-notice__icon svg{fill:#614200}.wcpay-inline-notice.components-notice.is-warning button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #614200}.wcpay-inline-notice.components-notice.is-warning .wcpay-inline-notice__action{color:#614200}.wcpay-inline-notice.components-notice.is-error{background-color:#fcf0f1}.wcpay-inline-notice.components-notice.is-error .wcpay-inline-notice__icon svg{fill:#8a2424}.wcpay-inline-notice.components-notice.is-error button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #8a2424}.wcpay-inline-notice.components-notice.is-error .wcpay-inline-notice__action{color:#8a2424}.wcpay-inline-notice.components-notice.is-success{background-color:#edfaef}.wcpay-inline-notice.components-notice.is-success .wcpay-inline-notice__icon svg{fill:#005c12}.wcpay-inline-notice.components-notice.is-success button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #005c12}.wcpay-inline-notice.components-notice.is-success .wcpay-inline-notice__action{color:#005c12}.wcpay-inline-notice.components-notice button.wcpay-inline-notice__action.is-link{text-decoration:none;box-shadow:none} .woocommerce-payments-page{margin:0 auto}.woocommerce-payments-page.is-narrow{max-width:680px} .wcpay-banner-notice{display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:13px;background-color:#fff;border-left:4px solid var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba));fill:var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba));margin:24px 0;padding:12px;box-shadow:0 2px 6px 0 rgba(0,0,0,.05)}.wcpay-banner-notice.is-success{border-left-color:#4ab866;fill:#4ab866}.wcpay-banner-notice.is-warning{border-left-color:#f0b849;fill:#f0b849}.wcpay-banner-notice.is-error{border-left-color:#cc1818;fill:#cc1818}.wcpay-banner-notice.is-warning .wcpay-banner-notice__icon,.wcpay-banner-notice.is-error .wcpay-banner-notice__icon{height:21px}.wcpay-banner-notice__icon{flex-shrink:0;margin-right:12px;margin-left:-2px}.wcpay-banner-notice__content{flex-grow:1}.wcpay-banner-notice__actions{display:grid;grid-auto-flow:column;grid-auto-columns:max-content;column-gap:12px;margin-top:12px}.wcpay-banner-notice__dismiss.components-button.has-icon{flex-shrink:0;padding:0;min-width:24px;height:24px}.wcpay-banner-notice__dismiss.components-button.has-icon svg{width:20px}.wcpay-banner-notice+.wcpay-banner-notice,.wcpay-banner-notice:first-child{margin-top:0} .wcpay-component-tip-box{display:grid;grid-template-columns:24px 1fr;column-gap:8px;padding:16px 12px;margin:16px 0}.wcpay-component-tip-box:first-child{margin-top:0}.wcpay-component-tip-box:last-child{margin-bottom:0}.wcpay-component-tip-box.purple{fill:#720eec;background-color:#f2edff}.wcpay-component-tip-box.blue{fill:#007cba;background-color:#f0f6fc}.wcpay-component-tip-box.gray{fill:#646970;background-color:#f6f7f7;color:#1e1e1e}.wcpay-component-tip-box.gray .components-button.is-link{color:#0063a1}.wcpay-component-tip-box.yellow{fill:#646970;background-color:#fef8ee;color:#1e1e1e}.wcpay-component-tip-box.yellow .components-button.is-link{color:#0063a1}.wcpay-component-tip-box__content{align-self:center}.wcpay-component-tip-box a{font-weight:700;color:#5007aa}.wcpay-component-tip-box .components-button.is-link{font-size:inherit;color:#720eec} #connect-account-page .connect-account-page h2{font-size:20px;line-height:28px;font-weight:400;color:#1d2327;margin:0}#connect-account-page .connect-account-page__heading{padding:24px 24px 0}#connect-account-page .connect-account-page__heading img{height:22px;margin-bottom:12px}#connect-account-page .connect-account-page__heading h2{margin:24px 0}@media screen and (min-width: 600px){#connect-account-page .connect-account-page__heading h2{font-size:32px;line-height:40px;font-weight:400;line-height:normal}}#connect-account-page .connect-account-page__content{padding:24px;padding-top:0}#connect-account-page .connect-account-page__payment-methods{padding:0 24px}#connect-account-page .connect-account-page__payment-methods p{font-size:11px;line-height:16px;color:#757575;margin-top:0}#connect-account-page .connect-account-page__payment-methods__icons{border:1px solid #ddd;border-bottom:none;padding:12px;display:grid;column-gap:8px;row-gap:16px;grid-template-columns:repeat(auto-fill, minmax(39px, 1fr))}#connect-account-page .connect-account-page__payment-methods__icons img{height:24px}#connect-account-page .connect-account-page__payment-methods__icons span{font-size:12px;line-height:16px;font-weight:400;color:#757575;display:none}@media screen and (min-width: 600px){#connect-account-page .connect-account-page__payment-methods__icons span{display:initial;white-space:nowrap;align-self:end}}#connect-account-page .connect-account-page__payment-methods__description{padding:12px;display:flex;flex-direction:column;gap:10px;border:1px solid #ddd}#connect-account-page .connect-account-page__payment-methods__description p{font-size:11px;line-height:16px;color:#1e1e1e;font-weight:600;text-transform:uppercase;margin-bottom:0}#connect-account-page .connect-account-page__payment-methods__description span{font-size:12px;line-height:16px;font-weight:400;color:#757575}#connect-account-page .connect-account-page__payment-methods__description__divider{border-top:1px solid #e0e0e0}@media screen and (min-width: 600px){#connect-account-page .connect-account-page__payment-methods__description__divider{border-right:1px solid #e0e0e0}}@media screen and (min-width: 600px){#connect-account-page .connect-account-page__payment-methods__description{flex-direction:row}}#connect-account-page .connect-account-page__buttons{padding:24px}#connect-account-page .connect-account-page__buttons button{width:100%;justify-content:center;margin-right:12px}@media screen and (min-width: 600px){#connect-account-page .connect-account-page__buttons button{width:auto;justify-content:left}}#connect-account-page .connect-account-page__buttons button.is-primary{background:#3858e9}#connect-account-page .connect-account-page__buttons button.is-primary:hover{background:#546ff3}#connect-account-page .connect-account-page__details{padding:36px 24px}#connect-account-page .connect-account-page__details .wcpay-component-tip-box{margin-top:24px}@media screen and (min-width: 600px){#connect-account-page .connect-account-page__steps{display:grid;grid-template-columns:repeat(3, 1fr);column-gap:20px}}#connect-account-page .connect-account-page__step{padding-top:8px;display:grid;grid-template-rows:auto 1fr auto}#connect-account-page .connect-account-page__step span{background-color:#f2edff;color:#6108ce;font-size:20px;font-weight:800;border-radius:12px;height:40px;width:40px;display:inline-block;text-align:center;line-height:40px;margin-bottom:8px}#connect-account-page .connect-account-page__step h3{font-size:14px;line-height:20px;font-weight:600;color:#1e1e1e;margin-bottom:0}#connect-account-page .connect-account-page__step p{font-size:12px;line-height:16px;font-weight:400;color:#2f2f2f;margin-top:4px}#connect-account-page .connect-account-page__incentive{background:#faf8ff;border:1px solid #ae7dd0;border-radius:2px;padding:24px;margin-bottom:24px}#connect-account-page .connect-account-page__incentive-pill{display:inline-block;padding:0 12px;border:1px solid #1d2327;border-radius:16px;text-transform:uppercase;font-size:12px;line-height:22px;font-weight:600;color:#1e1e1e;margin-bottom:12px}#connect-account-page .connect-account-page__incentive p{font-size:12px;line-height:16px;font-weight:400;color:#50575e}#connect-account-page .connect-account-page__incentive p:last-child{margin-bottom:0}@media screen and (min-width: 600px){#connect-account-page .connect-account-page__incentive h2{margin-right:10%}#connect-account-page .connect-account-page__incentive p{margin-right:15%}}#connect-account-page .connect-account-page__sandbox-mode-panel .components-panel__body.is-opened{padding:24px}#connect-account-page .connect-account-page__sandbox-mode-panel .components-panel__body.is-opened h2{margin:-24px -24px 5px}#connect-account-page .connect-account-page__sandbox-mode-panel h2 button{font-size:16px;line-height:24px;font-weight:600;padding:16px 40px 16px 24px !important}#connect-account-page .connect-account-page__sandbox-mode-panel h2 button:focus{box-shadow:none !important}#connect-account-page .connect-account-page__sandbox-mode-panel h2 button:hover{background:#fff !important}#connect-account-page .connect-account-page__sandbox-mode-panel .components-notice{margin-top:8px}#connect-account-page .connect-account-page__sandbox-mode-panel button.is-secondary{width:100%;justify-content:center}@media screen and (min-width: 600px){#connect-account-page .connect-account-page__sandbox-mode-panel button.is-secondary{width:auto;justify-content:left}}#connect-account-page .connect-account-page__preloader{position:fixed;width:100%;height:100%;top:0;left:0;background:#fff;z-index:999999;text-align:center}#connect-account-page .connect-account-page__preloader img.logo{position:absolute;width:64px;top:24px;left:24px}.connect-account-page__info-modal.components-modal__frame{max-width:700px}.connect-account-page__info-modal .components-notice{margin:16px 0 0;padding:12px 12px 0}.connect-account-page__info-modal hr{margin:24px 0}.connect-account-page__info-modal__footer{display:flex;justify-content:flex-end}.connect-account-page__info-modal__footer>*:not(:first-child){margin-left:16px} .woocommerce-payments__onboarding_location_check-modal{max-width:500px !important}.woocommerce-payments__onboarding_location_check-modal .woocommerce-payments__onboarding_location_check-modal-message{font-size:14px;line-height:21px;margin:0 0 24px 0}.woocommerce-payments__onboarding_location_check-modal .woocommerce-list{margin:.75rem 1rem}.woocommerce-payments__onboarding_location_check-modal .woocommerce-list .woocommerce-list__item{display:list-item;list-style:disc;padding:0;border:0}.woocommerce-payments__onboarding_location_check-modal .woocommerce-list .woocommerce-list__item:hover{background:none}.woocommerce-payments__onboarding_location_check-modal .woocommerce-list .woocommerce-list__item .woocommerce-list__item-inner{margin:0;padding:0}.woocommerce-payments__onboarding_location_check-modal .woocommerce-list .woocommerce-list__item .woocommerce-list__item-title{color:#50575e;margin-top:0;font-size:14px;font-size:0.875rem}.woocommerce-payments__onboarding_location_check-modal .woocommerce-list .woocommerce-list__item .woocommerce-list__item-content{color:#2c3338;margin-top:0;font-size:14px;font-size:0.875rem}.woocommerce-payments__onboarding_location_check-modal .woocommerce-payments__onboarding_location_check-footer{display:flex;justify-content:flex-end}.woocommerce-payments__onboarding_location_check-modal .woocommerce-payments__onboarding_location_check-footer>*:not(:first-child){margin-left:16px} .connect-account-page__payment-methods--logos{display:flex;align-items:center;padding:12px 0;gap:8px}.connect-account-page__payment-methods--logos img{width:38px;height:24px;border:1px solid #f0f0f0;border-radius:3px}.connect-account-page__payment-methods--logos-count{width:38px;height:24px;background-color:rgba(117,117,117,.1);color:#1e1e1e;text-align:center;line-height:24px;border-radius:3px;font-size:11px;font-weight:600}.connect-account-page__payment-methods--logos-count:hover{cursor:pointer;color:#3858e9;background-color:rgba(56,88,233,.15);border-color:#3858e9}.connect-account-page__payment-methods--logos-inside-popover{display:grid;grid-template-columns:repeat(4, 1fr);grid-template-rows:repeat(3, 1fr);gap:8px;padding:0}.connect-account-page__payment-methods--logos-popover .components-popover__content{background:#fff;border:1px solid #f0f0f0;border-radius:3px;max-width:246px;padding:8px;box-shadow:none;margin-top:8px} .wcpay-reset-account-modal{color:#1e1e1e;fill:#720eec}@media(max-height: 880px){.wcpay-reset-account-modal{max-height:100% !important}}.wcpay-reset-account-modal .components-modal__header{position:initial;padding:0;border:0}.wcpay-reset-account-modal .components-modal__header h1{font-size:20px;line-height:28px;font-weight:400;margin-bottom:8px}.wcpay-reset-account-modal .components-modal__content{box-sizing:border-box;max-width:600px;margin:auto;padding:8px 36px 36px}.wcpay-reset-account-modal__title{font-size:20px;line-height:28px;font-weight:400}.wcpay-reset-account-modal__content{display:grid;grid-template-columns:auto;gap:16px;padding:4px;align-items:normal;margin-bottom:24px}.wcpay-reset-account-modal__content ol{margin-top:0;margin-bottom:0}.wcpay-reset-account-modal__footer{display:flex;justify-content:flex-end}.wcpay-reset-account-modal__footer>*:not(:first-child){margin-left:16px} .wcpay-tooltip__content-wrapper{display:contents}.wcpay-tooltip__content-wrapper>div{display:inline}.wcpay-tooltip__content-wrapper [role=button]{cursor:pointer;display:inline-flex;align-items:center;color:#949494;transition:all .3s ease;fill:currentColor;margin:0 .4em;vertical-align:text-bottom}.wcpay-tooltip__content-wrapper [role=button]:focus,.wcpay-tooltip__content-wrapper [role=button]:hover,.wcpay-tooltip__content-wrapper [role=button]:active{outline:none;color:var(--wp-admin-theme-color, #007cba)}.wcpay-tooltip__tooltip-wrapper{visibility:hidden;position:fixed;opacity:0;transition:opacity 150ms ease-in;z-index:100010;left:0;white-space:normal;font-size:12px;font-weight:400}.wcpay-tooltip__tooltip-wrapper.is-hiding{opacity:0 !important;transition:opacity 200ms ease-out 300ms}.wcpay-tooltip__tooltip{position:relative;color:#fff;background-color:#1e1e1e;padding:10px;text-align:center}.wcpay-tooltip__tooltip a{color:var(--wp-admin-theme-color-background-25, #c5d9ed);text-decoration:underline}.wcpay-tooltip__tooltip ul{list-style:inherit;margin-left:20px}.wcpay-tooltip--click__content-wrapper{cursor:pointer}.wcpay-tooltip--click__tooltip{position:relative;padding:12px;color:#1e1e1e;text-align:left;border-radius:2px;border:1px solid #ccc;background-color:#fff;box-shadow:0 2px 6px 0 rgba(0,0,0,.05)}.wcpay-tooltip--click__tooltip a{color:var(--wp-admin-theme-color, #007cba)}.wcpay-tooltip--click__tooltip::after{display:none} .wcpay-setup-real-payments-modal.components-modal__frame{width:512px}@media screen and (max-width: 600px){.wcpay-setup-real-payments-modal.components-modal__frame{height:fit-content;margin:auto auto;max-width:90vw}}.wcpay-setup-real-payments-modal .components-modal__content{box-sizing:border-box;max-width:600px;margin:0;padding:8px 36px 36px}.wcpay-setup-real-payments-modal .components-modal__header{position:initial;padding:24px 0 16px 0;height:auto;border:0}.wcpay-setup-real-payments-modal .components-modal__header h1{font-size:20px;line-height:28px;font-weight:400;font-weight:300;margin-bottom:8px}.wcpay-setup-real-payments-modal__title{font-size:20px;line-height:28px;font-weight:400}.wcpay-setup-real-payments-modal__content{display:flex;gap:24px;flex-direction:column;padding:12px 0 16px 0}.wcpay-setup-real-payments-modal__content__item p{line-height:20px;margin:0}.wcpay-setup-real-payments-modal__content__item-flex{display:flex;gap:16px;padding-right:24px}.wcpay-setup-real-payments-modal__content__item-flex__description{color:#757575}.wcpay-setup-real-payments-modal__content__item-flex p{line-height:20px;margin:0}.wcpay-setup-real-payments-modal__footer{display:flex;justify-content:flex-end;padding-top:24px}.wcpay-setup-real-payments-modal__footer>*:not(:first-child){margin-left:16px} .sandbox-mode-notice .wcpay-banner-notice__content{display:flex} .woocommerce-table__clickable-cell{display:block;margin:-16px -24px;padding:16px 24px;color:inherit;text-decoration:none}.woocommerce-table__clickable-cell,.woocommerce-layout .woocommerce-table__clickable-cell,.woocommerce-layout .woocommerce-table__clickable-cell:link,.woocommerce-layout .woocommerce-table__clickable-cell:hover,.woocommerce-layout .woocommerce-table__clickable-cell:focus,.woocommerce-layout .woocommerce-table__clickable-cell:visited{color:#50575e;box-shadow:none} @media(max-width: 960px){.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-table__download-button,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-table__download-button{grid-area:1/2/2/3;justify-self:end;margin:-6px 0;position:absolute}}.woocommerce-report-table.has-compare.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-table__download-button,.woocommerce-report-table.has-search.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-table__download-button{align-self:center;grid-column-start:2;grid-column-end:3}@media(max-width: 960px){.woocommerce-report-table.has-compare.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-table__download-button,.woocommerce-report-table.has-search.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-table__download-button{grid-area:1/2/2/3}}button.woocommerce-table__download-button{padding:6px 12px;color:#000;text-decoration:none;align-items:center}button.woocommerce-table__download-button svg{margin-right:8px;height:24px;width:24px}@media(max-width: 960px){button.woocommerce-table__download-button svg{margin-right:0}button.woocommerce-table__download-button .woocommerce-table__download-button__label{display:none}} .chip{font-size:13px;font-size:0.8125rem;display:inline-flex;align-items:center;justify-content:center;font-weight:300;padding:.25rem .5rem;border-radius:.2rem;line-height:1.5;white-space:nowrap}.chip.chip-has-icon{padding-inline-start:4px;gap:2px}.chip.chip-primary{background:#f0f6fc;color:#0a4b78}.chip.chip-primary svg{fill:#0a4b78}.chip.chip-success{background:#edfaef;color:#005c12}.chip.chip-success svg{fill:#005c12}.chip.chip-light{background:#f6f7f7;color:#2c3338}.chip.chip-light svg{fill:#2c3338}.chip.chip-alert{background:#fcf0f1;color:#8a2424}.chip.chip-alert svg{fill:#8a2424}.chip.chip-warning{background:#fcf9e8;color:#614200}.chip.chip-warning svg{fill:#614200} .wcpay-deposits-list .woocommerce-table__header.is-numeric .components-button{margin-right:12px}button.components-button.wcpay-banner-notice__dismiss.has-icon{height:20px} .wcpay-merchant-feedback-prompt-wrap.components-snackbar-list{position:absolute;left:16px;right:16px;bottom:100%;margin-bottom:12px;width:auto}.wcpay-merchant-feedback-prompt-wrap.components-snackbar-list__notice-container,.wcpay-merchant-feedback-prompt-wrap.components-snackbar-list>div{transition:none !important;animation:none !important;transform:none !important;opacity:1 !important;height:auto !important}.wcpay-merchant-feedback-prompt-wrap .wcpay-merchant-feedback-prompt__action.components-button{color:inherit;text-decoration:none;display:flex;align-items:center}.wcpay-merchant-feedback-prompt-wrap .wcpay-merchant-feedback-prompt__action-label{margin-left:.3em;text-decoration:underline;text-underline-offset:3px}@media(max-width: 480px){.wcpay-merchant-feedback-prompt-wrap .wcpay-merchant-feedback-prompt__action.components-button{margin-right:10px}.wcpay-merchant-feedback-prompt-wrap .wcpay-merchant-feedback-prompt__action-label{display:none}}.wcpay-merchant-feedback-positive-modal .components-modal__content{max-width:600px}.wcpay-merchant-feedback-negative-modal__content{max-width:600px}.wcpay-merchant-feedback-negative-modal__question{font-weight:bold}.wcpay-merchant-feedback-negative-modal__textarea{width:100%;min-height:120px;padding:8px}.wcpay-merchant-feedback-negative-modal__privacy a{text-decoration:none}.wcpay-merchant-feedback-negative-modal__actions{display:flex;justify-content:flex-end;gap:8px} .wcpay-spotlight{position:fixed;bottom:24px;right:24px;z-index:999;width:300px;opacity:0;transform:translateX(100%);transition:opacity .3s ease-out,transform .3s ease-out;font-family:"SF Pro Display",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}.wcpay-spotlight--visible{opacity:1;transform:translateX(0)}@media screen and (max-width: 782px){.wcpay-spotlight{right:0;left:0;bottom:0;width:100%}.wcpay-spotlight .wcpay-spotlight__card{margin:0}.wcpay-spotlight .wcpay-spotlight__card.components-card{border-radius:0}}@media(prefers-reduced-motion: reduce){.wcpay-spotlight{transition:none}}.wcpay-spotlight__container{width:100%}.wcpay-spotlight__card{margin:0;position:relative;overflow:hidden}.wcpay-spotlight__card.components-card{border-radius:4px;box-shadow:0 2px 3px rgba(0,0,0,.05),0 4px 5px rgba(0,0,0,.04),0 4px 5px rgba(0,0,0,.03),0 16px 16px rgba(0,0,0,.02)}.wcpay-spotlight__card.has-image .wcpay-spotlight__header,.wcpay-spotlight__card.has-icon .wcpay-spotlight__header{position:absolute;top:0;right:0;width:100%;z-index:1;background:transparent}.wcpay-spotlight__card.has-image .wcpay-spotlight__image,.wcpay-spotlight__card.has-icon .wcpay-spotlight__image{padding:0}.wcpay-spotlight__header{padding:12px 12px 0}.wcpay-spotlight__header.components-card__header{border-radius:4px 4px 0 0}.wcpay-spotlight__header .wcpay-spotlight__badge,.wcpay-spotlight__header .wcpay-spotlight__heading{margin:0}.wcpay-spotlight__controls{width:100%}.wcpay-spotlight__close-btn{min-width:auto;width:32px;height:32px;padding:4px}.wcpay-spotlight__close-btn:hover{background:transparent}.wcpay-spotlight__close-btn:focus-visible{outline:2px solid #007cba;outline-offset:2px;box-shadow:none}.wcpay-spotlight__close-btn .components-icon{color:#1e1e1e}.wcpay-spotlight__image{padding:0;margin:0}.wcpay-spotlight__image img,.wcpay-spotlight__image svg{width:100%;height:auto;display:block}.wcpay-spotlight__icon{margin-bottom:8px;display:flex;align-items:flex-start}.wcpay-spotlight__icon svg{width:44px;height:44px;padding:6px;background:#fff;flex-shrink:0}.wcpay-spotlight__body{padding:16px}.wcpay-spotlight__card:not(.has-image):not(.has-icon) .wcpay-spotlight__body{padding-top:0}.wcpay-spotlight__card.has-icon .wcpay-spotlight__body{padding:16px}.wcpay-spotlight__badge{margin-bottom:12px}.wcpay-spotlight__heading{font-size:var(--l, 15px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);margin:0 0 12px;color:#1e1e1e}.wcpay-spotlight__description{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--s, 20px);color:#1e1e1e;margin:0}.wcpay-spotlight__description a{color:#2271b1;text-decoration:none}.wcpay-spotlight__description a:hover{text-decoration:underline}.wcpay-spotlight__footnote{font-size:var(--s, 12px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--xs, 16px);color:#1e1e1e;margin:12px 0 0}.wcpay-spotlight__footer{padding:0 16px 24px}.wcpay-spotlight__footer.components-card__footer{border-radius:0 0 4px 4px}.wcpay-spotlight__primary-btn:focus-visible,.wcpay-spotlight__secondary-btn:focus-visible{outline:2px solid #007cba;outline-offset:2px;box-shadow:none}body.woocommerce_page_wc-settings .wcpay-spotlight .wcpay-spotlight__card.components-surface.components-card{border-radius:4px;box-shadow:0 2px 3px rgba(0,0,0,.05),0 4px 5px rgba(0,0,0,.04),0 4px 5px rgba(0,0,0,.03),0 16px 16px rgba(0,0,0,.02)} .woocommerce-pill.wcpay-pill{border-color:#757575;color:#757575}.woocommerce-pill.wcpay-pill__light{border:0;background:#f0f0f1;color:#2c3338}.woocommerce-pill.wcpay-pill__alert{border:0;background:#fcf9e8;color:#614200}.woocommerce-pill.wcpay-pill__danger{border:0;background:#fcf0f1;color:#8a2424}.woocommerce-pill.wcpay-pill__success{border:0;background:#edfaef;color:#005c12}.woocommerce-pill.wcpay-pill__primary{border:0;background:#f0f6fc;color:#193a6f} .components-tooltip{background:#000;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;border-radius:2px;color:#f0f0f0;text-align:center;line-height:1.4;font-size:12px;padding:4px 8px;z-index:1000002;box-shadow:0 1px 2px rgba(0,0,0,.05),0 2px 3px rgba(0,0,0,.04),0 6px 6px rgba(0,0,0,.03),0 8px 8px rgba(0,0,0,.02)}.components-tooltip__shortcut{margin-left:8px}.transactions-list .date-time{min-width:195px}.transactions-list .converted-amount{display:flex;justify-content:flex-end}.transactions-list .converted-amount .conversion-indicator{margin-right:6px;fill:#8c8f94}.transactions-list .woocommerce-table__header.is-numeric .components-button{margin-right:12px}.transactions-list .woocommerce-table__header.is-center-aligned{text-align:center}.transactions-list .woocommerce-taptopay__icon{position:relative;display:inline-block;width:16px;height:16px;background:no-repeat url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUgMTFDNy4xMjE3MyAxMSA5LjE1NjU2IDExLjg0MjkgMTAuNjU2OSAxMy4zNDMxQzEyLjE1NzEgMTQuODQzNCAxMyAxNi44NzgzIDEzIDE5IiBzdHJva2U9IiMxRTFFMUUiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik01IDVDOC43MTMwMyA1IDEyLjI3NCA2LjQ3NSAxNC44OTk1IDkuMTAwNTFDMTcuNTI1IDExLjcyNiAxOSAxNS4yODcgMTkgMTkiIHN0cm9rZT0iIzFFMUUxRSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHJlY3QgeD0iNCIgeT0iMTciIHdpZHRoPSIzIiBoZWlnaHQ9IjMiIGZpbGw9IiMxRTFFMUUiLz4KPC9zdmc+Cg==);background-size:16px 16px;transform:rotate(45deg);cursor:pointer;top:3px;left:8px}.woocommerce-report-table .woocommerce-search{flex-grow:1}.woocommerce-report-table .woocommerce-card__header{position:relative}.woocommerce-report-table.has-compare .woocommerce-card__action,.woocommerce-report-table.has-search .woocommerce-card__action{align-items:center;text-align:left;display:grid;width:100%;grid-template-columns:auto 1fr auto}@media(max-width: 960px){.woocommerce-report-table.has-compare .woocommerce-card__action,.woocommerce-report-table.has-search .woocommerce-card__action{grid-gap:12px;grid-template-columns:auto 1fr;grid-row-start:2;grid-row-end:2;grid-column-start:1;grid-column-end:4;margin:0}.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-table__compare,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-table__compare{display:flex}.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-search,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-search{grid-area:2/2/3/4;margin-right:0}}.woocommerce-report-table.has-compare.has-search:not(.has-compare) .woocommerce-card__action,.woocommerce-report-table.has-search.has-search:not(.has-compare) .woocommerce-card__action{grid-template-columns:1fr auto}.woocommerce-report-table.has-compare.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-search,.woocommerce-report-table.has-search.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-search{align-self:center;grid-column-start:1;grid-column-end:2}@media(max-width: 960px){.woocommerce-report-table.has-compare.has-search:not(.has-compare) .woocommerce-card__action,.woocommerce-report-table.has-search.has-search:not(.has-compare) .woocommerce-card__action{grid-template-columns:auto}.woocommerce-report-table.has-compare.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-search,.woocommerce-report-table.has-search.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-search{grid-area:2/1/3/4;margin-left:0}}.woocommerce-report-table.has-compare .woocommerce-search,.woocommerce-report-table.has-search .woocommerce-search{margin:0 16px}.woocommerce-report-table.has-compare .woocommerce-search .components-base-control.woocommerce-select-control__control,.woocommerce-report-table.has-search .woocommerce-search .components-base-control.woocommerce-select-control__control{min-height:38px;height:auto}.woocommerce-report-table.has-compare .woocommerce-search .components-base-control.woocommerce-select-control__control button.woocommerce-select-control__clear,.woocommerce-report-table.has-search .woocommerce-search .components-base-control.woocommerce-select-control__control button.woocommerce-select-control__clear{background-color:#fff}.woocommerce-report-table.has-compare .woocommerce-search .components-base-control.woocommerce-select-control__control button.woocommerce-select-control__clear::before,.woocommerce-report-table.has-search .woocommerce-search .components-base-control.woocommerce-select-control__control button.woocommerce-select-control__clear::before{content:"";position:absolute;left:-30px;top:-2px;bottom:0;width:30px;height:100%;background:linear-gradient(to right, rgba(255, 255, 255, 0), white 90%);pointer-events:none}.woocommerce-report-table.has-compare .woocommerce-compare-button,.woocommerce-report-table.has-search .woocommerce-compare-button{padding:3px 12px;height:auto}@media(max-width: 660px){.components-snackbar-list{position:absolute;left:16px;right:16px;bottom:100%;margin-bottom:12px;width:auto}} .woopayments-copy-button{line-height:1.2em;display:inline-flex;background:transparent;border:none;border-radius:0;vertical-align:middle;font-weight:normal;cursor:pointer;color:inherit;margin-left:2px;align-items:center}.woopayments-copy-button i{display:block;width:1.2em;height:1.2em;mask-image:url(../assets/images/icons/copy.svg?ver=14a3a00de64a8c0c151c);mask-size:contain;mask-repeat:no-repeat;mask-position:center;background-color:currentColor}.woopayments-copy-button i:hover{opacity:.7}.woopayments-copy-button i:active{transform:scale(0.9)}.woopayments-copy-button.state--copied i{mask-image:url(../assets/images/icons/check-green.svg?ver=488deef5db84c71239e1);background-color:#008a20;animation:copy-indicator 2s forwards}@keyframes copy-indicator{0%{opacity:1}95%{opacity:1}100%{opacity:0}} .wcpay-deposit-overview .woocommerce-order-status{margin-bottom:4px;white-space:nowrap}.wcpay-deposit-overview .woocommerce-order-status__indicator{width:.85em;height:.85em}.wcpay-deposit-overview .woocommerce-order-status__indicator.is-paid{background:#00ba37;border-color:#b8e6bf}.wcpay-deposit-overview .woocommerce-order-status__indicator.is-pending{background:#f0c33c;border-color:#f5e6ab}.wcpay-deposit-overview .woocommerce-order-status__indicator.is-in_transit{background:#4f94d4;border-color:#c5d9ed}.wcpay-deposit-overview .woocommerce-order-status__indicator.is-failed{background:#d63638;border-color:#ffabaf}.wcpay-deposit-overview .woocommerce-summary__item .woocommerce-summary__item-label{font-size:14px;line-height:20px}.wcpay-deposit-overview .woocommerce-summary__item .woocommerce-summary__item-value{font-size:20px;line-height:28px}.wcpay-deposit-overview .woocommerce-summary__item .wcpay-summary__item-detail{color:#555d66;word-wrap:break-word}.wcpay-deposit-overview .woocommerce-summary__item .wcpay-summary__item-detail .woopayments-payout-bank-reference-id{font-family:monospace}.wcpay-deposit-overview .wcpay-deposit-fee{color:#8a2424}.wcpay-deposit-overview .wcpay-deposit-net{color:#005c12}.wcpay-deposit-overview .woopayments-payout-details-header{display:flex;flex-wrap:wrap;justify-content:space-between;gap:.75rem}.wcpay-deposit-overview .woopayments-payout-details-header__bank-reference-id{font-family:monospace;color:#646970}.wcpay-deposit-overview .woopayments-payout-details-header__value{color:#646970;align-self:center}@media(max-width: 660px){.wcpay-deposit-overview .woopayments-payout-details-header__value{align-self:flex-start}}.wcpay-deposit-overview .woopayments-payout-details-header__item{display:flex}@media(max-width: 660px){.wcpay-deposit-overview .woopayments-payout-details-header__item{flex-direction:column}}.wcpay-deposit-overview .woopayments-payout-details-header h2{font-size:.6875rem;font-weight:500;color:#1d2327;min-width:11rem;text-transform:uppercase}.wcpay-deposit-overview .wcpay-deposit-automatic ul{display:flex;margin:0;list-style-type:none}@media(max-width: 660px){.wcpay-deposit-overview .wcpay-deposit-automatic ul{flex-direction:column}}.wcpay-deposit-overview .wcpay-deposit-automatic ul .woocommerce-summary__item{border-bottom:0;background-color:inherit;color:inherit}.wcpay-deposit-overview .wcpay-deposit-automatic ul .woocommerce-summary__item .woocommerce-summary__item-label:hover{color:inherit}@media(max-width: 660px){.wcpay-deposit-overview .wcpay-deposit-automatic ul .woocommerce-summary__item{border-right:none}}.wcpay-deposit-overview .wcpay-deposit-automatic ul .wcpay-deposit-amount{flex-grow:1;padding:24px;text-align:right;font-size:36px;line-height:82px}@media(max-width: 660px){.wcpay-deposit-overview .wcpay-deposit-automatic ul .wcpay-deposit-amount{line-height:36px;text-align:left;border-top:1px solid #dcdcde}}.wcpay-deposit-overview--instant__transactions-list-message.components-card__body{padding:24px}.wcpay-deposit-overview .woocommerce-summary .woocommerce-summary__item-label{margin-bottom:12px}.wcpay-deposit-overview .woocommerce-summary .woocommerce-summary__item-data{margin-top:0;margin-bottom:8px}.wcpay-deposit-overview .woocommerce-summary .wcpay-summary__item-detail{color:#555d66}.wcpay-deposit-overview .woocommerce-summary .woocommerce-summary__item-prev-value,.wcpay-deposit-overview .woocommerce-summary .woocommerce-summary__item-delta{display:none}.wcpay-deposit-overview .woocommerce-summary .woocommerce-summary__item{background:#fff}.wcpay-deposit-overview .woocommerce-summary .woocommerce-summary__item:hover .woocommerce-summary__item-label{color:#757575}.wcpay-deposit-overview .woocommerce-summary a.woocommerce-summary__item:hover{background-color:#f6f7f7}.wcpay-deposit-overview .woocommerce-summary a.woocommerce-summary__item:hover:hover .woocommerce-summary__item-label{color:var(--wp-admin-theme-color)}.wcpay-deposit-overview .woocommerce-summary a.woocommerce-summary__item:active{background-color:#dcdcde} .wcpay-transactions-page .wcpay-banner-notice.is-warning{margin-bottom:4px}.wcpay-transactions-page .components-tab-panel__tabs{box-shadow:inset 0 -1px 0 #ccc;margin-bottom:24px}.wcpay-transactions-page .components-tab-panel__tabs .components-tab-panel__tabs-item.active-tab,.wcpay-transactions-page .components-tab-panel__tabs .components-tab-panel__tabs-item:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) transparent,inset 0 -4px 0 0 var(--wp-admin-theme-color)} .card-notice{background-color:#fff8ee}.card-notice__section{display:flex;justify-content:space-between;width:100%;align-items:center}@media(max-width: 660px){.card-notice__section{flex-direction:column;gap:10px}} .payment-method-details{gap:5px}.payment-method-details .payment-method__icon{width:28px;width:1.75rem}.payment-method-details .payment-method__brand{width:28px;width:1.75rem;height:17px;height:1.063rem;margin-bottom:-4px;margin-bottom:-0.25rem;margin-top:1px}@media screen and (max-width: 782px){.payment-method-details__brand-tooltip{display:none}} .woocommerce-list--horizontal{margin:0 -16px 0 -16px}.woocommerce-list--horizontal,.woocommerce-list--horizontal{display:flex;align-items:stretch;flex-direction:column}@media(min-width: 801px){.woocommerce-list--horizontal,.woocommerce-list--horizontal{flex-direction:initial;margin:1rem 0 0 0}}.woocommerce-list--horizontal .woocommerce-list__item,.woocommerce-list--horizontal .woocommerce-list__item{padding:.75rem 1rem;border-left:0;border-bottom:1px solid #dcdcde;border-top:0}@media(min-width: 801px){.woocommerce-list--horizontal .woocommerce-list__item,.woocommerce-list--horizontal .woocommerce-list__item{padding:0 1rem;border-left:1px solid #dcdcde;border-bottom:0}}.woocommerce-list--horizontal .woocommerce-list__item:hover,.woocommerce-list--horizontal .woocommerce-list__item:hover{background:none}.woocommerce-list--horizontal .woocommerce-list__item:hover .woocommerce-list__item-title,.woocommerce-list--horizontal .woocommerce-list__item:hover .woocommerce-list__item-title{color:#50575e}.woocommerce-list--horizontal .woocommerce-list__item .woocommerce-list__item-inner,.woocommerce-list--horizontal .woocommerce-list__item .woocommerce-list__item-inner{margin:0;padding:0}.woocommerce-list--horizontal .woocommerce-list__item .woocommerce-list__item-title,.woocommerce-list--horizontal .woocommerce-list__item .woocommerce-list__item-content,.woocommerce-list--horizontal .woocommerce-list__item .woocommerce-list__item-title,.woocommerce-list--horizontal .woocommerce-list__item .woocommerce-list__item-content{margin-top:0;font-size:14px;font-size:0.875rem}.woocommerce-list--horizontal .woocommerce-list__item .woocommerce-list__item-title,.woocommerce-list--horizontal .woocommerce-list__item .woocommerce-list__item-title{text-transform:uppercase;color:#757575;font-size:11px;font-weight:600}.woocommerce-list--horizontal .woocommerce-list__item .woocommerce-list__item-content,.woocommerce-list--horizontal .woocommerce-list__item .woocommerce-list__item-content{color:#2c3338;display:flex}@media(min-width: 801px){.woocommerce-list--horizontal .woocommerce-list__item:first-child,.woocommerce-list--horizontal .woocommerce-list__item:first-child{border-left:0;padding-left:0}}.woocommerce-list--horizontal .woocommerce-list__item:last-child,.woocommerce-list--horizontal .woocommerce-list__item:last-child{border-bottom:0;padding-bottom:0}@media(min-width: 801px){.woocommerce-list--horizontal .woocommerce-list__item:last-child,.woocommerce-list--horizontal .woocommerce-list__item:last-child{padding-right:0}} .is-loadable-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#e2e4e7;color:transparent;display:inline-block}@media screen and (prefers-reduced-motion: reduce){.is-loadable-placeholder{animation:none}}.is-loadable-placeholder.is-inline{display:inline}.is-loadable-placeholder.is-block{display:block;margin-top:1em}.is-loadable-placeholder.is-block:first-child{margin-top:0}.is-loadable-placeholder.is-block p{margin:0} .woocommerce-payments-page .components-card__header{font-size:20px}.woocommerce-payments-page .components-card{margin-bottom:24px}.payment-details-summary{display:flex;flex:1}@media screen and (max-width: 782px){.payment-details-summary{flex-direction:column}}.payment-details-summary .payment-details-summary__section{flex-grow:1}.payment-details-summary .payment-details-summary__amount-wrapper{display:flex;align-items:center}@media screen and (max-width: 600px){.payment-details-summary .payment-details-summary__amount-wrapper{flex-direction:column;align-items:flex-start}.payment-details-summary .payment-details-summary__amount-wrapper .payment-details-summary__status{order:-1}}.payment-details-summary .payment-details-summary__amount{font-size:32px;font-size:2rem;font-weight:300;padding:0;margin:0;display:flex;flex-wrap:nowrap;align-items:center}.payment-details-summary .payment-details-summary__amount .payment-details-summary__amount-currency{font-size:16px;font-size:1rem;text-transform:uppercase;margin:0 .5rem;color:#50575e}.payment-details-summary .payment-details-summary__breakdown__settlement-currency{padding-right:16px;border-right:1px solid #e0e0e0}.payment-details-summary .payment-details-summary__breakdown p{font-size:14px;font-size:0.875rem;color:#757575;display:inline-flex;margin:.25rem 1rem 0 0;text-transform:uppercase;font-weight:600;font-size:12px}.payment-details-summary .payment-details-summary__breakdown p:last-child{margin-right:0}.payment-details-summary .payment-details-summary__breakdown__fee-tooltip{display:flex;flex-direction:column;padding:12px;gap:8px;font-weight:400;font-size:14px}.payment-details-summary .payment-details-summary__breakdown__fee-tooltip>*:last-child{padding-top:8px;border-top:1px solid #e0e0e0}.payment-details-summary .payment-details-summary__breakdown__fee-tooltip label{margin-right:16px;color:#757575}.payment-details-summary .payment-details-summary__fraud-outcome-action{display:flex;justify-content:flex-end}.payment-details-summary .payment-details-summary__fraud-outcome-action button+button{margin-left:8px}.payment-details-summary .payment-details-summary__id{height:100%;margin-top:8px;color:#787c82;display:flex;align-items:start;justify-content:initial;flex-direction:column;flex-wrap:nowrap}@media screen and (min-width: 782px){.payment-details-summary .payment-details-summary__id{justify-content:flex-start;align-items:flex-end}}.payment-details-summary .payment-details-summary__id .payment-details-summary__id_wrapper{white-space:nowrap}.payment-details-summary .payment-details-summary__id .payment-details-summary__id_value{font-family:monospace}.woocommerce-list--horizontal{margin-top:0 !important}.payment-details__refund-controls{flex:0 0 auto}.payment-details__refund-controls .refund-controls__dropdown-menu .components-popover{position:fixed}.payment-details__refund-controls .refund-controls__dropdown-menu .components-popover__content{position:absolute;right:100%}.payment-details__refund-controls .refund-controls__dropdown-menu .components-tooltip .components-popover__content{position:relative;right:0} .missing-order-notice-modal__reason .components-base-control__label{font-weight:bold}.missing-order-notice-modal__reason .components-radio-control__option label{line-height:2} .transaction-details-dispute-details-wrapper{padding-left:24px;padding-right:24px;padding-bottom:5px}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-title{margin-top:24px;margin-bottom:24px;font-size:var(--l, 15px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);color:var(--Scales-Grays-gray-900, #1e1e1e)}@media screen and (max-width: 600px){.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-title{margin-top:16px;margin-bottom:16px}}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body{padding-bottom:16px;margin-bottom:3px}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body .wcpay-inline-notice{padding:0}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body .wcpay-inline-notice.components-notice .components-notice__content{letter-spacing:.02em;font-size:14px;line-height:1.5em;margin:16px}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body .dispute-summary-row{font-size:13px;font-weight:400;margin-top:40px}@media screen and (max-width: 600px){.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body .dispute-summary-row{margin-top:24px}}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body__visa-compliance-checkbox{margin:0}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body__visa-compliance-checkbox input[type=checkbox]{appearance:checkbox}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body__actions{display:flex;justify-content:start;gap:8px;margin-top:24px}@media screen and (max-width: 600px){.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body__actions{flex-direction:column}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body__actions .components-button{width:100%;justify-content:center}}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body__help-link{margin:24px 0 40px;text-align:left}@media screen and (max-width: 600px){.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body__help-link{margin-top:24px 0}}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body__help-link .components-external-link{color:var(--wp-admin-theme-color, #007cba);text-decoration:none}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body__help-link .components-external-link:hover{text-decoration:underline}.transaction-details-dispute-details-wrapper .transaction-details-dispute-details-body__help-link .components-external-link .components-external-link__icon{margin-bottom:1.5px;margin-left:2.5px}.dispute-reason-tooltip p{margin:0;margin-bottom:8px}.dispute-reason-tooltip p:last-child{margin-bottom:0}.dispute-steps{margin-top:40px}.dispute-steps__item{display:flex;align-items:center;padding:16px 0 16px;border-bottom:1px solid #f0f0f0}.dispute-steps__item:first-child{padding:12px 0 16px}.dispute-steps__item:last-child{border-bottom:none;padding-bottom:28px}@media screen and (max-width: 600px){.dispute-steps__item{flex-direction:column;padding:16px 0;gap:12px}}.dispute-steps__item-icon{display:flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:var(--radius-s, 2px);padding:10px;border:1px solid #e0e0e0;margin-right:12px}@media screen and (max-width: 600px){.dispute-steps__item-icon{display:none}}.dispute-steps__item-icon svg{width:24px;height:24px;color:var(--Scales-Grays-gray-700, #757575);fill:currentColor}.dispute-steps__item-content{flex:1;min-width:0}@media screen and (max-width: 600px){.dispute-steps__item-content{width:100%}}.dispute-steps__item-name{margin-bottom:4px;color:var(--Scales-Grays-gray-900, #1e1e1e);letter-spacing:.01em;font-size:var(--m, 13px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px)}.dispute-steps__item-description{color:var(--Scales-Grays-gray-700, #757575);font-size:var(--m, 13px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--s, 20px)}@media screen and (max-width: 600px){.dispute-steps__item-description{margin-bottom:8px;width:100%}}.dispute-steps__item-content:last-child .dispute-steps__item-description{width:90%}@media screen and (max-width: 600px){.dispute-steps__item-content:last-child .dispute-steps__item-description{margin-bottom:8px;width:100%}}.dispute-steps__item-action{margin-left:16px;flex-shrink:0}.dispute-steps__item-action a{padding:4px 10px;max-height:32px}@media screen and (max-width: 600px){.dispute-steps__item-action{margin-left:0;width:100%}.dispute-steps__item-action .components-button{width:100%;justify-content:center}}.dispute-steps__steps__response-date--urgent{color:var(--Gutenberg-Alert-Red, #cc1818);font-weight:600;font-size:13px}.dispute-steps__notice .dispute-steps__notice-content .components-notice__content{letter-spacing:.02em;margin:8px 24px 8px 0;font-size:14px;line-height:1.5em}@media screen and (max-width: 600px){.dispute-steps__notice .dispute-steps__notice-content .components-notice__content{margin:8px 16px 8px 0;line-height:1.4}}.transaction-details-dispute-accept-modal.components-modal__frame{max-width:600px}.transaction-details-dispute-accept-modal .components-modal__content{padding-top:24px}.transaction-details-dispute-accept-modal__icon{flex-shrink:0;padding:6px;margin-right:8px}.transaction-details-dispute-accept-modal__actions{margin-top:24px}.transaction-details-dispute-footer{color:#757575;background-color:#f2f4f5}.transaction-details-dispute-footer__actions{flex-shrink:0}.transaction-details-dispute-footer--primary{background-color:#f0f6fc}@media screen and (max-width: 600px){.transaction-details-dispute-footer .components-flex{flex-direction:column;align-items:flex-start}.transaction-details-dispute-footer .components-flex-item{margin:10px 0}}.dispute-evidence.components-panel__body{border:none}.dispute-evidence.components-panel__body.is-opened{padding-bottom:0}.dispute-evidence .components-panel__body-title button{text-transform:uppercase;color:#757575;font-size:11px;font-weight:600}.dispute-evidence .components-panel__body-toggle.components-button{padding:10px}.dispute-evidence .components-panel__body-toggle.components-button:focus{box-shadow:none;outline:0}.dispute-evidence__list{list-style:none;margin:8px 0 0}.dispute-evidence__list-item{display:inline-block;margin-right:12px;margin-bottom:0}.dispute-notice .dispute-notice__link{display:block}@media screen and (max-width: 600px){.wcpay-inline-notice.components-notice .components-notice__content a.dispute-notice__link{white-space:normal}} .wcpay-accordion{background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:8px}.wcpay-accordion.is-high-density{border-radius:0;padding:0}.wcpay-accordion>.wcpay-accordion__body:first-child{margin-top:-1px}.wcpay-accordion>.wcpay-accordion__body:last-child{border-bottom-width:0}.wcpay-accordion+.wcpay-accordion{margin-top:-1px}.wcpay-accordion__body{border-bottom:1px solid #e0e0e0}.wcpay-accordion__body.is-opened{padding:16px}.wcpay-accordion__body+.wcpay-accordion__body{margin-top:-1px}.wcpay-accordion__body>.wcpay-accordion__body-title{display:block;padding:0;font-size:inherit;margin-top:0;margin-bottom:0;transition:.3s background ease-in-out}.wcpay-accordion__body.is-opened>.wcpay-accordion__body-title{margin:-16px -16px 0 -16px}.wcpay-accordion__body>.wcpay-accordion__body-title:hover{border:none}.wcpay-accordion__body-toggle.components-button{position:relative;padding:16px 48px 16px 16px;outline:none;width:100%;font-weight:500;text-align:left;color:#1e1e1e;border:none;box-shadow:none;transition:.2s background ease-in-out;height:auto}.wcpay-accordion__body-toggle.components-button .wcpay-accordion__title-content{display:flex;flex-direction:column;gap:4px}.wcpay-accordion__body-toggle.components-button.is-md{font-size:13px}.wcpay-accordion__body-toggle.components-button.is-lg{font-size:15px}.wcpay-accordion__body-toggle.components-button:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba));border-radius:0}.wcpay-accordion__body-toggle.components-button .wcpay-accordion__arrow{position:absolute;right:16px;top:50%;transform:translateY(-50%);color:#1e1e1e;fill:currentColor;transition:.2s color ease-in-out}body.rtl .wcpay-accordion__body-toggle.components-button .dashicons-arrow-right{transform:scaleX(-1);-ms-filter:fliph;filter:FlipH;margin-top:-10px}.wcpay-accordion__subtitle{color:#757575;font-size:13px;font-style:normal;font-weight:400;line-height:20px}.wcpay-accordion__icon{color:#757575;margin:-2px 0 -2px 6px}.wcpay-accordion__body-toggle-icon{margin-right:-5px}.wcpay-accordion__color-title{float:left;height:19px}.wcpay-accordion__row{display:flex;justify-content:space-between;align-items:center;margin-top:8px;min-height:36px}.wcpay-accordion__row select{min-width:0}.wcpay-accordion__row label{margin-right:12px;flex-shrink:0;max-width:75%}.wcpay-accordion__row:empty,.wcpay-accordion__row:first-of-type{margin-top:0}.wcpay-accordion .circle-picker{padding-bottom:20px} .wcpay-confirmation-modal.wcpay-confirmation-modal{max-width:600px}.wcpay-confirmation-modal .components-modal__content{display:flex;flex-direction:column;padding:0 24px 24px}.wcpay-confirmation-modal .components-modal__content p{padding:0 0 1em 0;margin:0}.wcpay-confirmation-modal .components-modal__content p.no-padding{padding:0}.wcpay-confirmation-modal .components-modal__header{padding:24px}.wcpay-confirmation-modal__separator{margin:24px -24px}.wcpay-confirmation-modal__footer{display:flex;justify-content:flex-end}.wcpay-confirmation-modal__footer>*:not(:first-child){margin-left:16px} .woocommerce-timeline .woocommerce-timeline-item__body .fee-breakdown-list,.woocommerce-timeline .woocommerce-timeline-item__body .discount-split-list{margin-left:16px;line-height:1.5}.woocommerce-timeline .woocommerce-timeline-item__body .fee-breakdown-list{list-style-type:disc}.woocommerce-timeline .woocommerce-timeline-item__body .discount-split-list{list-style-type:circle}.woocommerce-timeline .woocommerce-timeline-item__headline svg.is-fraud-outcome-review{background:#d67709}.woocommerce-timeline .woocommerce-timeline-item .fraud-outcome-ruleset-item+.fraud-outcome-ruleset-item{color:#50575e;font-size:14px;line-height:20px;margin-top:16px} .wcpay-transaction-breakdown,.wcpay-transaction-breakdown__footer,.wcpay-transaction-breakdown div,.wcpay-transaction-breakdown__footer div{font-size:16px}.wcpay-transaction-breakdown__authorized_payment_amount{text-align:right;font-weight:600}@media screen and (max-width: 782px){.wcpay-transaction-breakdown__authorized_payment_amount{width:100%}}.wcpay-transaction-breakdown__conversion_rate{margin-top:0 !important;font-size:12px !important;color:#787c82;text-align:right;line-height:16px;font-weight:normal}.wcpay-transaction-breakdown__fees{padding-left:24px;margin-top:0 !important;padding-top:0 !important;color:#787c82;width:100%}@media screen and (max-width: 782px){.wcpay-transaction-breakdown__fees{padding-left:0}}.wcpay-transaction-breakdown__fee_info{padding-top:.5rem}@media screen and (max-width: 782px){.wcpay-transaction-breakdown__fee_info{margin-left:0;padding-top:1.5rem}}.wcpay-transaction-breakdown__fees-container{margin-left:0;padding-left:0;width:100%}.wcpay-transaction-breakdown__fees .wcpay-transaction-breakdown__fee_name{width:45%}@media screen and (max-width: 782px){.wcpay-transaction-breakdown__fees .wcpay-transaction-breakdown__fee_name{width:100%;text-align:left}}.wcpay-transaction-breakdown__fees .wcpay-transaction-breakdown__fee_rate{width:45%;text-align:right;margin-right:0}@media screen and (max-width: 782px){.wcpay-transaction-breakdown__fees .wcpay-transaction-breakdown__fee_rate{width:100%;text-align:left}}.wcpay-transaction-breakdown__total_transaction_fee{padding-top:1rem}@media screen and (max-width: 782px){.wcpay-transaction-breakdown__total_transaction_fee_amount{width:100%;text-align:right}}.wcpay-transaction-breakdown__footer{font-weight:600}.wcpay-transaction-breakdown__footer_amount{text-align:right}@media screen and (max-width: 782px){.wcpay-transaction-breakdown__footer_amount{width:fit-content}} .wcpay-disputes-list .woocommerce-table__header.is-center-aligned{text-align:center}.wcpay-disputes-list .woocommerce-table__item a.components-button{display:inline-flex}.wcpay-disputes-list .woocommerce-pagination{align-items:flex-end}.wcpay-dispute-details .components-card__header,.wcpay-dispute-evidence .components-card__header{font-size:20px}.wcpay-dispute-details .components-card,.wcpay-dispute-evidence .components-card{margin-bottom:24px}.wcpay-dispute-details .components-button+.components-button,.wcpay-dispute-evidence .components-button+.components-button{margin-left:10px}.wcpay-dispute-details .components-textarea-control__input,.wcpay-dispute-evidence .components-textarea-control__input{display:block;margin-top:4px;width:100%}.wcpay-dispute-details .components-text-control__input,.wcpay-dispute-evidence .components-text-control__input{margin-top:4px}.wcpay-dispute-details .components-base-control,.wcpay-dispute-evidence .components-base-control{position:relative}.wcpay-dispute-details .components-base-control .components-base-control__label,.wcpay-dispute-evidence .components-base-control .components-base-control__label{margin-bottom:4px}.wcpay-dispute-details .components-base-control+.components-base-control,.wcpay-dispute-evidence .components-base-control+.components-base-control{margin:16px 0 0}.wcpay-dispute-details .components-base-control>.components-base-control__help,.wcpay-dispute-evidence .components-base-control>.components-base-control__help{font-style:normal;font-weight:400;font-size:12px;line-height:16px;color:#787c82;margin-top:8px}.wcpay-dispute-details .file-upload,.wcpay-dispute-evidence .file-upload{display:flex;flex-direction:row;align-items:center}.wcpay-dispute-details .file-upload .components-button.is-secondary:not(:disabled):not([aria-disabled=true]).is-success,.wcpay-dispute-evidence .file-upload .components-button.is-secondary:not(:disabled):not([aria-disabled=true]).is-success{box-shadow:inset 0 0 0 1px #008a20;color:#008a20}.wcpay-dispute-details .file-upload .components-button.is-secondary:not(:disabled):not([aria-disabled=true]).is-success:hover,.wcpay-dispute-evidence .file-upload .components-button.is-secondary:not(:disabled):not([aria-disabled=true]).is-success:hover{box-shadow:inset 0 0 0 1px #007017;color:#007017}.wcpay-dispute-details .file-upload .components-button.is-secondary:not(:disabled):not([aria-disabled=true]).is-destructive,.wcpay-dispute-evidence .file-upload .components-button.is-secondary:not(:disabled):not([aria-disabled=true]).is-destructive{box-shadow:inset 0 0 0 1px #d63638;color:#d63638}.wcpay-dispute-details .file-upload .components-button.is-secondary:not(:disabled):not([aria-disabled=true]).is-destructive:hover,.wcpay-dispute-evidence .file-upload .components-button.is-secondary:not(:disabled):not([aria-disabled=true]).is-destructive:hover{box-shadow:inset 0 0 0 1px #b32d2e;color:#b32d2e}.wcpay-dispute-details .file-upload .components-button.delete-uploaded-file-button:not(:disabled):not([aria-disabled=true]):hover,.wcpay-dispute-evidence .file-upload .components-button.delete-uploaded-file-button:not(:disabled):not([aria-disabled=true]):hover{color:#b32d2e;box-shadow:none}.wcpay-dispute-details .file-upload .upload-message,.wcpay-dispute-evidence .file-upload .upload-message{padding-left:24px;color:#787c82}.wcpay-dispute-details .file-upload .upload-message.is-destructive,.wcpay-dispute-evidence .file-upload .upload-message.is-destructive{color:#d63638} .wcpay-dispute-detail-legacy-redirect{top:10vh;position:relative}.wcpay-dispute-detail-legacy-redirect .components-flex-item{text-align:center} .stepper-panel{display:flex;gap:24px;align-items:center;justify-content:center;width:100%;padding:24px;box-sizing:border-box;min-width:0}.stepper-step{display:flex;justify-content:space-around;gap:16px;align-items:center;position:relative}.stepper-circle{width:24px;height:24px;min-width:24px;display:flex;align-items:center;justify-content:center;border:1px solid #e0e0e0;z-index:1;transition:background .2s,border .2s,color .2s;text-align:center;font-size:13px;font-style:normal;font-weight:600;color:#757575;line-height:16px;padding:4px 8px;border-radius:50%;background:none;cursor:default}.stepper-circle:disabled{cursor:default}.stepper-step.clickable .stepper-circle,.stepper-step.clickable .stepper-label{cursor:pointer}.stepper-step.clickable .stepper-circle:disabled,.stepper-step.clickable .stepper-label:disabled{cursor:default}.stepper-step.active .stepper-circle{border-color:var(--wp-admin-theme-color);color:var(--wp-admin-theme-color)}.stepper-step.complete .stepper-label{color:var(--wp-admin-theme-color);font-weight:600}.stepper-step.complete .stepper-circle{background:var(--wp-admin-theme-color);color:#fff;border-color:var(--wp-admin-theme-color);padding:0}.stepper-step.complete .stepper-circle svg{fill:#fff}.stepper-label{color:#757575;background:none;border:none;padding:0;text-align:left;font-size:13px;font-style:normal;font-weight:400;line-height:16px;cursor:default}.stepper-label:disabled{cursor:default}.stepper-step.active .stepper-label{font-weight:600}.stepper-line{left:100%;width:48px;height:1px;background:#e0e0e0;z-index:0}@media(max-width: 782px){.stepper-panel{margin-top:24px;margin-bottom:24px;justify-content:space-around;padding:0 .5rem}.stepper-line{display:none}.stepper-label{display:none}} .wcpay-dispute-evidence-new{display:flex;flex-direction:column;gap:24px}.wcpay-dispute-evidence-new__stepper-divider{margin:0}.wcpay-dispute-evidence-new__stepper-section{background:#fff;border:1px solid #e0e0e0;padding:0 24px 128px 24px}.wcpay-dispute-evidence-new__stepper-content{max-width:500px;margin-left:auto;margin-right:auto}.wcpay-dispute-evidence-new__stepper-title{font-size:var(--xl, 20px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--m, 24px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin-top:24px;margin-bottom:12px}.wcpay-dispute-evidence-new__stepper-subheading{color:var(--Scales-Grays-gray-700, #757575);font-size:var(--m, 13px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--s, 20px)}.wcpay-dispute-evidence-new__cover-letter-warning{margin-bottom:16px;border-left:4px solid var(--wp-components-color-warning, #f0b849)}.wcpay-dispute-evidence-new__button-row{display:flex;justify-content:space-between;margin-top:40px}.wcpay-dispute-evidence-new__button-group-right{display:flex;align-items:center}.wcpay-dispute-evidence-new .components-card{margin-bottom:24px}.wcpay-dispute-evidence-new .components-button+.components-button{margin-left:10px}.wcpay-dispute-evidence-new__button-group-right .components-button+.components-button{margin-left:8px}.wcpay-dispute-evidence-new .dispute-steps__notice-content{font-size:14px}.wcpay-dispute-evidence-customer-details{margin-top:40px;margin-bottom:40px}.wcpay-dispute-evidence-customer-details__heading{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin-bottom:16px}.wcpay-dispute-evidence-customer-details__row{display:flex;align-items:flex-start;gap:24px 32px;margin-bottom:24px;flex-wrap:wrap}.wcpay-dispute-evidence-customer-details__row>div{flex:0 0 auto;min-width:70px;display:flex;flex-direction:column;align-items:flex-start;gap:8px;align-self:stretch}.wcpay-dispute-evidence-customer-details__row>div>span{color:var(--Scales-Grays-gray-700, #757575);font-size:var(--s, 12px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--xs, 16px)}.wcpay-dispute-evidence-customer-details__label,.wcpay-dispute-evidence-customer-details__billing-label{color:var(--Scales-Grays-gray-900, #1e1e1e);text-transform:uppercase;font-size:var(--xs, 11px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--xs, 16px)}.wcpay-dispute-evidence-customer-details__link,.wcpay-dispute-evidence-customer-details__ip-address{font-size:var(--s, 12px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--xs, 16px)}.wcpay-dispute-evidence-customer-details__link{color:var(--wp-admin-theme-color);text-decoration:underline;word-break:break-all}.wcpay-dispute-evidence-customer-details__billing{margin-top:8px;display:flex;flex-direction:column;align-items:flex-start;gap:8px;align-self:stretch}.wcpay-dispute-evidence-customer-details__billing-value{color:var(--Scales-Grays-gray-900, #757575);font-size:var(--s, 12px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--xs, 16px);word-break:break-word}@media(max-width: 700px){.wcpay-dispute-evidence-customer-details__row{flex-direction:column;gap:8px;margin-bottom:8px}.wcpay-dispute-evidence-customer-details__row>div{min-width:0}}.wcpay-dispute-evidence-product-details{margin-bottom:40px}.wcpay-dispute-evidence-product-details__heading{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin-bottom:8px}.wcpay-dispute-evidence-product-details__field-group{margin-bottom:24px}.wcpay-dispute-evidence-product-details__subheading{color:var(--Scales-Grays-gray-700, #757575);font-size:var(--s, 12px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--xs, 16px);margin-bottom:16px}.wcpay-dispute-evidence-shipping-details{margin-top:40px;margin-bottom:40px}.wcpay-dispute-evidence-shipping-details__heading{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin-bottom:8px}.wcpay-dispute-evidence-shipping-details__field-group{margin-bottom:24px}.wcpay-dispute-evidence-shipping-details__subheading{color:var(--Scales-Grays-gray-700, #757575);font-size:var(--s, 12px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--xs, 16px);margin-bottom:24px}.wcpay-dispute-evidence-visa-compliance{margin-bottom:40px;margin-top:40px}.wcpay-dispute-evidence-visa-compliance__heading{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin-bottom:8px}.wcpay-dispute-evidence-visa-compliance__subheading{color:var(--Scales-Grays-gray-700, #757575);font-size:var(--s, 12px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--xs, 16px);margin-bottom:24px}.wcpay-dispute-evidence-visa-compliance__textarea{margin-bottom:16px}.wcpay-dispute-evidence-recommended-documents{margin-bottom:40px}.wcpay-dispute-evidence-recommended-documents__heading{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin-bottom:8px}.wcpay-dispute-evidence-recommended-documents__subheading{color:var(--Scales-Grays-gray-700, #757575);font-size:var(--s, 12px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--xs, 16px)}.wcpay-dispute-evidence-recommended-documents__item{padding-bottom:16px;padding-top:10px;margin-bottom:0;border-bottom:1px solid var(--Scales-Grays-gray-100, #f0f0f0)}.wcpay-dispute-evidence-recommended-documents__item:last-child{border-bottom:none;margin-bottom:0;padding-bottom:10px}.wcpay-dispute-evidence-recommended-documents__list{margin-top:16px;margin-bottom:0}.wcpay-dispute-evidence-recommended-documents__helper-link{margin-top:8px}.wcpay-dispute-evidence-refund-status{margin-bottom:40px}.wcpay-dispute-evidence-refund-status__heading{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin-bottom:8px}.wcpay-dispute-evidence-duplicate-status{margin-bottom:40px}.wcpay-dispute-evidence-duplicate-status__heading{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin-bottom:8px}.wcpay-dispute-evidence-file-upload-control{display:flex;align-items:center;gap:10px;justify-content:space-between}.wcpay-dispute-evidence-file-upload-control__chip{display:flex;align-items:center;height:32px;max-height:32px;border-radius:var(--radius-full, 999px);background:var(--Scales-Theme-theme-alpha-04, rgba(56, 88, 233, 0.04))}.wcpay-dispute-evidence-file-upload-control__chip-filename{color:var(--wp-admin-theme-color);padding-left:8px;white-space:nowrap;display:flex;align-items:center;gap:0;max-width:200px}.wcpay-dispute-evidence-file-upload-control__chip-filename-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:1;min-width:0}.wcpay-dispute-evidence-file-upload-control__chip-filename-extension{flex-shrink:0;white-space:nowrap}.wcpay-dispute-evidence-file-upload-control__info{display:flex;flex-direction:column;align-items:flex-start}.wcpay-dispute-evidence-file-upload-control__info-header{height:32px;max-height:32px;display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px}.wcpay-dispute-evidence-file-upload-control__info-description{color:var(--Scales-Grays-gray-700, #757575);font-size:var(--s, 12px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--xs, 16px);margin:0}@media(max-width: 385px){.wcpay-dispute-evidence-file-upload-control__info{flex-direction:column;align-items:flex-start;gap:4px}.wcpay-dispute-evidence-file-upload-control__chip-filename{max-width:160px}}@media(min-width: 386px)and (max-width: 480px){.wcpay-dispute-evidence-file-upload-control__chip-filename{max-width:120px}.wcpay-dispute-evidence-file-upload-control__chip-filename-extension{display:none}}@media(min-width: 481px)and (max-width: 768px){.wcpay-dispute-evidence-file-upload-control__chip-filename{max-width:160px}}@media(min-width: 769px){.wcpay-dispute-evidence-file-upload-control__chip-filename{max-width:250px}}.wcpay-dispute-evidence-cover-letter{margin-top:40px;margin-bottom:40px}.wcpay-dispute-evidence-cover-letter__textarea{margin-bottom:16px}.wcpay-dispute-evidence-cover-letter__printonly{display:none}.wcpay-accordion .wcpay-inline-notice.components-notice .components-notice__content{font-size:14px}@media print{.wcpay-dispute-evidence-cover-letter{max-width:700px;margin:0 auto !important;background:#fff;z-index:9999;padding:40px;box-sizing:border-box;page-break-inside:avoid}.wcpay-dispute-evidence-cover-letter__print,.wcpay-dispute-evidence-cover-letter__textarea{display:none !important}.wcpay-dispute-evidence-cover-letter__printonly{display:block !important;white-space:pre-wrap;font-family:inherit;font-size:16px;color:#000;background:#fff;border:none;margin:0;padding:0;text-align:left;width:100%}}.evidence-summary__body .woocommerce-list--horizontal .woocommerce-list__item-enter,.evidence-summary__body .woocommerce-list--horizontal .woocommerce-list__item-enter-active,.evidence-summary__body .woocommerce-list--horizontal .woocommerce-list__item-enter-done,.evidence-summary__body .woocommerce-list--horizontal .woocommerce-list__item-exit,.evidence-summary__body .woocommerce-list--horizontal .woocommerce-list__item-exit-active,.evidence-summary__body .woocommerce-list--horizontal .woocommerce-list__item-exit-done,.evidence-summary__body .woocommerce-list--horizontal .woocommerce-list__item{transition:none;animation:none;transform:none;opacity:1;max-height:none}.wcpay-dispute-evidence-confirmation{padding:40px 0;display:flex;justify-content:center;background:#fff}.wcpay-dispute-evidence-confirmation__wrapper{width:984px;max-width:calc(100vw - 48px);padding:24px;display:flex;flex-direction:column;align-items:center;gap:24px}.wcpay-dispute-evidence-confirmation__content{display:flex;flex-direction:column;max-width:600px;width:100%}.wcpay-dispute-evidence-confirmation__illustration{display:flex;justify-content:center;margin-bottom:40px}.wcpay-dispute-evidence-confirmation__illustration-image{width:180px;height:auto;max-width:100%}.wcpay-dispute-evidence-confirmation__title{font-size:var(--xl, 20px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--m, 24px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin:0 0 12px 0;text-align:left}.wcpay-dispute-evidence-confirmation__subtitle{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--s, 20px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin:0 0 40px 0;text-align:left}.wcpay-dispute-evidence-confirmation__next-steps,.wcpay-dispute-evidence-confirmation__resources{text-align:left;width:100%;margin-bottom:24px}.wcpay-dispute-evidence-confirmation__next-steps h3,.wcpay-dispute-evidence-confirmation__resources h3{font-size:var(--xl, 20px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--m, 24px);color:var(--Scales-Grays-gray-900, #1e1e1e);margin:0 0 12px 0}.wcpay-dispute-evidence-confirmation__next-steps ul,.wcpay-dispute-evidence-confirmation__resources ul{list-style:none;padding:0 0 0 8px;margin:0}.wcpay-dispute-evidence-confirmation__next-steps ul li,.wcpay-dispute-evidence-confirmation__resources ul li{padding-left:12px;position:relative;font-size:var(--m, 13px);font-style:normal;font-weight:var(--Regular, 400);line-height:var(--s, 20px);color:var(--Scales-Grays-gray-900, #1e1e1e)}.wcpay-dispute-evidence-confirmation__next-steps ul li::before,.wcpay-dispute-evidence-confirmation__resources ul li::before{content:"•";font-weight:bold;position:absolute;left:0}.wcpay-dispute-evidence-confirmation__next-steps ul li a,.wcpay-dispute-evidence-confirmation__resources ul li a{color:#3858e9;text-decoration:none}.wcpay-dispute-evidence-confirmation__next-steps ul li a:hover,.wcpay-dispute-evidence-confirmation__resources ul li a:hover{text-decoration:underline}.wcpay-dispute-evidence-confirmation__notice.wcpay-inline-notice.components-notice{margin-bottom:48px}.wcpay-dispute-evidence-confirmation__actions{display:flex;gap:16px;justify-content:space-between;flex-wrap:wrap}@media(max-width: 768px){.wcpay-dispute-evidence-confirmation{padding:24px 16px}.wcpay-dispute-evidence-confirmation__title{font-size:24px}.wcpay-dispute-evidence-confirmation__actions{flex-direction:column-reverse;gap:12px;width:100%}.wcpay-dispute-evidence-confirmation__actions .components-button{width:100%;justify-content:center}.wcpay-dispute-evidence-confirmation__actions .components-button+.components-button{margin-left:0}} .task-collapsible-body{max-height:0;overflow:hidden;opacity:0;transition:max-height 500ms ease-out,opacity 500ms ease-out}.task-collapsible-body.is-active{overflow:visible;max-height:1000px;opacity:1;transition:max-height 500ms ease-in,opacity 500ms ease-in} .wcpay-wizard-task{margin-bottom:0}.wcpay-wizard-task:first-child .wcpay-wizard-task__top-border{display:none}.wcpay-wizard-task:last-child .wcpay-wizard-task__body{border-left:none}.wcpay-wizard-task__top-border,.wcpay-wizard-task__body{min-height:16px;border-left:1px solid #e0e0e0;margin:0 11px}.wcpay-wizard-task__headline{display:flex;align-items:center;flex-direction:row;margin:4px 0;font-weight:600;font-size:16px;color:#2c3338}.is-active .wcpay-wizard-task__headline{color:var(--wp-admin-theme-color)}.wcpay-wizard-task__title{padding:0 16px;display:flex;align-items:center;flex-wrap:wrap}.wcpay-wizard-task__title>*:not(:last-child){margin-right:4px}.wcpay-wizard-task__icon-wrapper{position:relative;width:24px;height:24px;padding:0;border-radius:9999px;border:1px solid #e0e0e0;display:flex;justify-content:center;align-items:center;flex-shrink:0}.is-active .wcpay-wizard-task__icon-wrapper{border-color:var(--wp-admin-theme-color)}.is-completed .wcpay-wizard-task__icon-wrapper{border-color:var(--wp-admin-theme-color);background-color:var(--wp-admin-theme-color)}.wcpay-wizard-task__icon-text{font-size:13px}.is-completed .wcpay-wizard-task__icon-text{opacity:0}.wcpay-wizard-task__icon-checkmark{opacity:0;transition:opacity 500ms ease;position:absolute;top:0;right:0;width:100%;height:100%;fill:#fff}.is-completed .wcpay-wizard-task__icon-checkmark{opacity:1}.wcpay-wizard-task__body{display:flex;flex-direction:column;padding:0 30px;font-size:13px}.wcpay-wizard-task__description-element{margin-top:0;margin-bottom:1em}.wcpay-wizard-task__description-element.is-muted-color{color:#757575}.wcpay-wizard-task__description-element .components-external-link svg{width:1em;height:1em}.wcpay-wizard-task__visible-description-element{position:absolute;margin-left:40px;margin-top:0;margin-bottom:1em}.wcpay-wizard-task__visible-description-element.is-muted-color{color:#757575}.wcpay-wizard-task__visible-description-element .components-external-link svg{width:1em;height:1em}.wcpay-wizard-task .add-payment-methods-task__payment-selector-wrapper{margin-bottom:24px}.wcpay-wizard-task .add-payment-methods-task__payment-selector-title{font-weight:600;font-size:16px}.wcpay-wizard-task .add-payment-methods-task__payment-selector-description{margin-bottom:24px} .woocommerce-payments__payment-method-icon{display:inline-flex;align-items:center;vertical-align:middle;max-width:160px}@media(min-width: 661px){.woocommerce-payments__payment-method-icon{max-width:inherit}}.woocommerce-payments__payment-method-icon__label{margin-left:4px}.woocommerce-payments__payment-method-icon svg{width:38px;height:auto}.woocommerce-payments__payment-method-icon .payment-method__icon{order:initial;margin-right:3px;max-width:45px} .wcpay-settings-layout{margin:24px auto 0;max-width:1032px;display:flex;flex-flow:column}@media(min-width: 961px){.wcpay-settings-layout{padding:0 56px}}body.woocommerce-payments-checkout-section .wcpay-settings-layout{margin:24px 0 0;max-width:1184px}@media(min-width: 961px){body.woocommerce-payments-checkout-section .wcpay-settings-layout{padding:0 30px}} .settings-section{display:flex;flex-flow:column}.settings-section:not(.settings-section:last-child){margin-bottom:70px}@media(min-width: 801px){.settings-section{flex-flow:row}}.settings-section__details{flex:0 1 auto;margin-bottom:24px;max-width:100%}@media(min-width: 801px){.settings-section__details{flex:0 0 33%;margin:0 24px 0 0;max-width:328px}}.settings-section__details h2{font-size:var(--l, 15px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);font-size:16px;line-height:24px;margin-top:0}.settings-section__details p{font-size:13px;line-height:17.89px;margin:12px 0}.settings-section__details>:last-child{margin-bottom:0}.settings-section__icons>li{display:inline-block;margin:0 5px 0 0}.settings-section__icons svg{border-radius:2px;box-shadow:0 0 0 1px #ddd;height:auto;width:38px}.settings-section__controls{flex:1 1 auto}.settings-section__controls .components-radio-control:not(:last-child),.settings-section__controls .components-base-control:not(:last-child){margin-bottom:24px} .multi-currency-store-settings-preview-modal{width:calc(100vw - 64px);min-width:calc(100vw - 64px);min-height:calc(100vh - 64px);display:flex;flex-direction:column}.multi-currency-store-settings-preview-modal .components-modal__content{padding:0;flex:1;display:flex;flex-direction:column}.multi-currency-store-settings-preview-modal .components-modal__content .components-modal__header{margin:0;padding:0 32px}.multi-currency-store-settings-preview-modal .components-modal__content iframe{flex:1;width:100%;height:100%} .multi-currency-settings-task__wrapper{margin-top:24px;display:flex;flex-direction:column;gap:16px} .setup-complete-task__buttons .components-button:not(:last-child){margin-right:8px}.setup-complete-task__enabled-methods-list{display:flex;align-items:center}.setup-complete-task__enabled-methods-list>*:not(:last-child){margin-right:8px} .add-currencies-task__currency-selector-wrapper.components-card .components-card__body{padding-left:0;padding-right:0}.add-currencies-task__currency-selector-wrapper.components-card .components-card__body h4,.add-currencies-task__currency-selector-wrapper.components-card .components-card__body .add-currencies-task__content .enabled-currency-checkbox,.add-currencies-task__currency-selector-wrapper.components-card .components-card__body .add-currencies-task__search{padding-left:16px;padding-right:16px}.add-currencies-task__currency-selector-wrapper.components-card .components-card__body .is-loadable-placeholder{margin-left:16px;margin-right:16px}.add-currencies-task__separator{border-bottom:1px solid #dcdcde;font-size:.1px;width:100%;background-color:#fff;height:1px;padding:16px 0 0 0;margin:0}.add-currencies-task__content{max-height:400px;min-height:400px;overflow-y:auto}.add-currencies-task__content .enabled-currency-checkbox{margin-top:-5px;padding-top:5px}.add-currencies-task__content .enabled-currency-checkbox__flag{font-size:20px;margin:0 5px 0 0}.add-currencies-task__content .enabled-currency-checkbox__flag-text{display:inline-block;width:20px;height:15px;line-height:15px;vertical-align:top;margin:0 .07em;background-color:#dcdcde;border-radius:2px;text-align:center}.add-currencies-task__content .enabled-currency-checkbox__flag-text span{font-size:9px;vertical-align:bottom}.add-currencies-task__content .enabled-currency-checkbox__code{color:#8c8f94}.add-currencies-task__content .components-checkbox-control__label{font-size:15px} .multi-currency-setup-wizard{max-width:700px;min-width:510px;margin:0 auto}.multi-currency-setup-wizard strong{font-weight:500} .wcpay-card-readers-page .components-tab-panel__tabs{box-shadow:inset 0 -1px 0 #ccc}.wcpay-card-readers-page .components-tab-panel__tabs .components-tab-panel__tabs-item.active-tab,.wcpay-card-readers-page .components-tab-panel__tabs .components-tab-panel__tabs-item:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) transparent,inset 0 -4px 0 0 var(--wp-admin-theme-color)}.wcpay-card-readers-page .card-readers-settings__wrapper .components-base-control__field{margin-bottom:15px}.wcpay-card-readers-page .card-readers-settings__wrapper .components-base-control__field .components-base-control__label{width:100%}.wcpay-card-readers-page .card-readers-settings__wrapper .components-base-control__field .components-text-control__input,.wcpay-card-readers-page .card-readers-settings__wrapper .components-base-control__field .components-input-control__container{padding:9px 8px;max-width:320px}.wcpay-card-readers-page .card-readers-settings__wrapper .components-base-control__field .components-input-control__container{padding:3px 0}.wcpay-card-readers-page .card-readers-settings__wrapper .components-base-control__field .block-editor-color-gradient-control__color-indicator .components-base-control__label{margin-left:-2px}.wcpay-card-readers-page .card-readers-settings__wrapper .components-base-control__field .block-editor-color-gradient-control__color-indicator .component-color-indicator{position:relative;top:3px;margin-left:.5rem}.wcpay-card-readers-page .card-readers-settings__wrapper .components-base-control__field .components-circular-option-picker .components-circular-option-picker__swatches{display:none}.wcpay-card-readers-page .card-readers-settings__wrapper .components-base-control__field .components-circular-option-picker .components-circular-option-picker__custom-clear-wrapper{display:block;margin-top:0}.wcpay-card-readers-page .card-readers-settings__wrapper .components-base-control__field .components-circular-option-picker button{text-decoration:none}.wcpay-card-readers-page .card-readers-settings__wrapper .wcpay-branding-help-label{font-style:normal;font-weight:400;font-size:12px;line-height:16px;color:#787c82;margin-top:8px}.wcpay-card-readers-page .card-readers-settings__wrapper .components-notice{margin:0 0 10px 0}.wcpay-card-readers-page .card-readers-settings-save-settings-section{text-align:right}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-list__header{display:flex}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-list__header div{flex:1}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-list__header .card-readers-list__header-status{max-width:25%;text-align:right}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-list__body{padding:0}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-add-new-reader{padding:20px 24px}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-item{border-bottom:1px solid #dcdcde;display:flex;flex-wrap:wrap;padding:13px 24px;margin:0}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-item div{flex:1}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-item div span{line-height:22px}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-item__id,.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-item__type{color:rgba(60,60,67,.6)}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-item__status{text-align:right;max-width:25%}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-item__status span{background:#dcdcde;border-radius:16px;align-items:center;padding:2px 8px}.wcpay-card-readers-page .card-readers-list__wrapper .card-readers-item__status .active{background:#b8e6bf} .wcpay-loan-summary-header .components-button.is-link{text-decoration:none}.wcpay-loan-summary-body{padding:0 !important}.wcpay-loan-summary-row+.wcpay-loan-summary-row{border-top:1px solid #e0e0e0}.wcpay-loan-summary-row.is-bottom-row{padding-top:24px;padding-bottom:24px}.wcpay-loan-summary-row.is-bottom-row .wcpay-loan-summary-block{padding-top:0;padding-bottom:0}.wcpay-loan-summary-block{padding:24px 16px;margin-right:0}.wcpay-loan-summary-block+.wcpay-loan-summary-block{border-left:1px solid #e0e0e0}.wcpay-loan-summary-block__title{font-size:12px;line-height:16px;color:#949494}.wcpay-loan-summary-block__value{color:#1e1e1e;font-size:13px;line-height:20px;padding:8px 0}.wcpay-loan-summary-block__value .is-big{font-size:20px;line-height:27px} .wcpay-loans-list .is-center-aligned{text-align:center}.wcpay-loans-list .woocommerce-pagination{display:none} div.wcpay-instant-payout-modal{max-width:500px}div.wcpay-instant-payout-modal p{color:#787c82}div.wcpay-instant-payout-modal ul{border:1px solid #dcdcde;border-radius:5px;font-weight:bold}div.wcpay-instant-payout-modal ul li{padding:1rem;clear:both;margin:0}div.wcpay-instant-payout-modal ul li span{float:right}div.wcpay-instant-payout-modal ul .wcpay-instant-payout-modal__balance{padding-bottom:0}div.wcpay-instant-payout-modal ul .wcpay-instant-payout-modal__fee span{color:#d63638}div.wcpay-instant-payout-modal ul .wcpay-instant-payout-modal__net{border-top:1px solid #dcdcde}div.wcpay-instant-payout-modal ul .wcpay-instant-payout-modal__net span{color:#00ba37}div.wcpay-instant-payout-modal__footer{display:flex;justify-content:flex-end}div.wcpay-instant-payout-modal__footer>*:not(:first-child){margin-left:16px} .wcpay-account-balances__balances__item{flex-direction:column;align-items:flex-start;flex:1;padding-left:16px}.wcpay-account-balances__balances__item__title{font-size:12px;line-height:16px;color:#949494;display:flex;align-items:center;margin-top:0}.wcpay-account-balances__balances__item__amount{font-weight:500;font-size:20px;margin:0;color:#1e1e1e}@media(max-width: 660px){.wcpay-account-balances__balances__item{padding-left:0}.wcpay-account-balances__balances__item:not(:first-child){padding-top:16px}}.wcpay-account-balances__instant-deposit-notice .components-notice__content{margin-right:0}.wcpay-account-balances__instant-deposit{border-top:1px solid #e0e0e0;padding:24px}.wcpay-account-balances__balances{display:flex;margin-bottom:0}.wcpay-account-balances__balances>*+*{border-left:1px solid #e0e0e0}.wcpay-account-balances__balances .wcpay-account-balances__balances__item__tooltip{line-height:20px}.wcpay-account-balances__balances .wcpay-account-balances__balances__item__tooltip a{text-decoration:none}.wcpay-account-balances__balances .wcpay-account-balances__balances-total-balance-tooltip-notice{margin-top:10px;margin-bottom:10px}.wcpay-account-balances__balances .wcpay-account-balances__balances-total-balance-tooltip-notice.components-notice{padding:8px}.wcpay-account-balances__balances .wcpay-account-balances__balances-total-balance-tooltip-notice .components-notice__content{font-size:12px;white-space:nowrap;color:#0a4b78;margin-right:0}@media(max-width: 660px){.wcpay-account-balances__balances{flex-direction:column;gap:16px}.wcpay-account-balances__balances>*+*{border-left:none;border-top:1px solid #e0e0e0}}.components-card.wcpay-account-balances{margin-bottom:0;box-shadow:none} .woopayments-account-details__header{flex-wrap:wrap}.woopayments-account-details__header .edit-details .components-button.is-link{text-decoration:none}.woopayments-account-details__header .edit-details,.woopayments-account-details__header .account-details,.woopayments-account-details__header .account-status{flex-basis:auto;min-width:max-content}@media screen and (max-width: 480px){.woopayments-account-details__header .account-details{order:1;flex-grow:1;margin-bottom:8px}.woopayments-account-details__header .edit-details{order:2;margin-bottom:8px}.woopayments-account-details__header .account-status{order:3}.woopayments-account-details__header .woopayments-account-details__banner{order:5;margin-bottom:16px}.woopayments-account-details__header .woopayments-account-details__banner a.components-external-link{white-space:wrap}}.woopayments-account-details__payout-status-item{line-height:16px;flex-wrap:wrap;height:auto;min-height:2em;margin-bottom:8px}.woopayments-account-details__payout-status-item .item-label,.woopayments-account-details__payout-status-item .item-value{flex-shrink:1;flex-basis:auto;width:min-content}.woopayments-account-details__payout-status-item .item-label{color:#1e1e1e;min-width:130px;white-space:nowrap}@media screen and (max-width: 480px){.woopayments-account-details__payout-status-item .item-label{min-width:80px}}.woopayments-account-details__payout-status-item .item-value{color:#757575}.woopayments-account-details__payout-status-item a.payout-tooltip-external-link{text-decoration:none} .account-tools{padding-top:16px}.account-tools__actions{display:grid;grid-auto-flow:column;grid-auto-columns:max-content;column-gap:12px;margin-top:12px} .progressbar{display:flex;max-width:600px}.progressbar .progressbar__container{flex-grow:1;display:flex;flex-flow:wrap;margin:8px 8px 8px 0;background-color:#fff;height:32px;border:1px #1e1e1e solid;border-radius:4px}.progressbar .progressbar__inner{background-color:#007cba;display:inline-flex;flex-direction:row-reverse;height:100%}.progressbar .progressbar__outer-progress-label{align-self:center;margin:4px}.progressbar .progressbar__inner-progress-label{color:#fff;align-self:center;margin:4px}.progressbar .progressbar__total-label{align-self:center} .wcpay-tooltip__tooltip-wrapper .wcpay-fees-tooltip{display:flex;flex-direction:column;gap:4px;padding:5px}.wcpay-tooltip__tooltip-wrapper .wcpay-fees-tooltip>div{display:flex;text-align:left;gap:16px;align-items:center;justify-content:space-between}.wcpay-tooltip__tooltip-wrapper .wcpay-fees-tooltip>div>div{white-space:nowrap}.wcpay-tooltip__tooltip-wrapper .wcpay-fees-tooltip__bold{font-weight:600}.wcpay-tooltip__tooltip-wrapper .wcpay-fees-tooltip__hint-text{font-size:95%;color:#ccc;margin-top:8px} .account-fees{padding-top:16px} .components-modal__frame.woopayments-connection-success-modal{max-width:512px}@media screen and (max-width: 782px){.components-modal__frame.woopayments-connection-success-modal{max-width:343px;margin:auto}}.components-modal__frame.woopayments-connection-success-modal .components-modal__header .components-button.has-icon{padding:0}.components-modal__frame.woopayments-connection-success-modal .components-modal__header .components-button.has-icon svg{width:30px;height:30px}.components-modal__frame.woopayments-connection-success-modal .components-modal__header-heading{font-size:20px;line-height:28px;font-weight:300;text-wrap:auto;color:#1e1e1e}.components-modal__frame.woopayments-connection-success-modal .woopayments-connection-success-modal__content{display:flex;gap:24px;flex-direction:column;padding:12px 0 16px 0;line-height:20px;font-weight:400;color:#1e1e1e}.components-modal__frame.woopayments-connection-success-modal .woopayments-connection-success-modal__actions{flex-direction:row-reverse;padding:24px 0 0 0;display:flex;gap:12px} .wcpay-deposits-overview__heading{display:flex;flex-direction:column;gap:8px;padding-bottom:16px}.wcpay-deposits-overview__heading__title{font-weight:600;font-size:16px;line-height:24px;color:#1e1e1e}.wcpay-deposits-overview .wcpay-inline-notice.components-notice{margin:16px 0}.wcpay-deposits-overview .components-card__body.wcpay-deposits-overview__schedule__container{padding-top:24px;padding-bottom:0}.wcpay-deposits-overview .components-card__body.wcpay-deposits-overview__schedule__container .is-loadable-placeholder{margin-bottom:24px}.wcpay-deposits-overview .components-card__body.wcpay-deposits-overview__table__container,.wcpay-deposits-overview .components-card__body.wcpay-deposits-overview__notices__container{padding-top:0}.wcpay-deposits-overview .components-card__body.wcpay-deposits-overview__table__container:not(:last-child),.wcpay-deposits-overview .components-card__body.wcpay-deposits-overview__notices__container:not(:last-child){padding-bottom:0}.wcpay-deposits-overview .wcpay-deposits-overview__table__cell:first-child{display:flex}.wcpay-deposits-overview .wcpay-inline-notice.components-notice.wcpay-deposit-transit-days-notice{background-color:#f6f7f7}.wcpay-deposits-overview .wcpay-inline-notice.components-notice.wcpay-deposit-transit-days-notice .wcpay-inline-notice__icon svg{fill:#1e1e1e}.wcpay-deposits-overview__table__row__header{font-weight:600}.wcpay-deposits-overview__table__row__header div{padding:8px 0}.wcpay-deposits-overview__table__cell{font-size:13px;align-items:center;padding:16px 0}.wcpay-deposits-overview__table__cell svg{fill:#1e1e1e;position:relative;left:2.5px;margin-right:24px}.wcpay-deposits-overview__table__cell:first-child{width:35%}.wcpay-deposits-overview__table__cell:last-child{margin-left:auto}.wcpay-deposits-overview__footer.components-card__footer{padding:24px;flex-wrap:wrap;justify-content:flex-start;align-items:center}@media screen and (max-width: 550px){.wcpay-deposits-overview__footer.components-card__footer{flex-direction:column;justify-content:center;padding:16px 24px}.wcpay-deposits-overview__footer.components-card__footer>*{margin:0}}@media screen and (max-width: 605px){.wcpay-deposits-overview__table__cell:first-child{width:45%}} .wcpay-filter.components-custom-select-control{font-size:13px;color:#1e1e1e}.wcpay-filter.components-custom-select-control .wcpay-filter.components-custom-select-control__label{display:inline-block;margin-bottom:0;color:#787c82;margin-right:.2em;white-space:nowrap}.wcpay-filter.components-custom-select-control .wcpay-filter.components-custom-select-control__label::after{content:":"}.wcpay-filter.components-custom-select-control .wcpay-filter.components-custom-select-control__item{padding:12px;margin:0;line-height:initial;grid-template-columns:auto auto auto;justify-content:start;white-space:nowrap;border-radius:2px}.wcpay-filter.components-custom-select-control .wcpay-filter.components-custom-select-control__item.is-highlighted{background:#f6f7f7}.wcpay-filter.components-custom-select-control .wcpay-filter.components-custom-select-control__item-icon{margin-right:.2em}.wcpay-filter.components-custom-select-control .wcpay-filter.components-custom-select-control__item-hint{margin-left:1.8em;text-align:left;color:#757575}.wcpay-filter.components-custom-select-control button.wcpay-filter.components-custom-select-control__button{width:100%;background-color:#fff;margin:0 1px;display:flex;justify-content:space-between;align-items:center;padding:6px 8px;border:1px solid #e0e0e0;font-size:13px}.wcpay-filter.components-custom-select-control button.wcpay-filter.components-custom-select-control__button:hover,.wcpay-filter.components-custom-select-control button.wcpay-filter.components-custom-select-control__button:focus{color:initial;background-color:#f6f7f7;box-shadow:none}.wcpay-filter.components-custom-select-control button.wcpay-filter.components-custom-select-control__button.placeholder{color:#646970}.wcpay-filter.components-custom-select-control button.wcpay-filter.components-custom-select-control__button .wcpay-filter.components-custom-select-control__button-value{text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden}.wcpay-filter.components-custom-select-control button.wcpay-filter.components-custom-select-control__button svg{fill:initial;width:18px;flex:0 0 18px}.wcpay-filter.components-custom-select-control button.wcpay-filter.components-custom-select-control__button[aria-expanded=true] .wcpay-filter.components-custom-select-control__button-value{visibility:hidden}.wcpay-filter.components-custom-select-control button.wcpay-filter.components-custom-select-control__button[aria-expanded=true] .components-custom-select-control__label::after{visibility:hidden}@media screen and (max-width: 782px){.wcpay-filter.components-custom-select-control button.wcpay-filter.components-custom-select-control__button{font-size:16px}}.wcpay-filter.components-custom-select-control .wcpay-filter.components-custom-select-control__menu{margin:-1px 1px;border-color:#ddd;max-height:300px;padding:8px 12px} .wcpay-welcome.components-card__header{padding:0;margin-bottom:24px;border:0}.wcpay-welcome__flex__greeting{margin-top:8px;margin-bottom:8px}@media screen and (max-width: 480px){.components-flex.wcpay-welcome__flex{flex-direction:column}.wcpay-welcome__flex__greeting{margin-bottom:24px}} .wcpay-inbox-message-enter{opacity:0;max-height:0;transform:translateX(50%)}.wcpay-inbox-message-enter-active{opacity:1;max-height:100vh;transform:translateX(0%);transition:opacity 500ms,transform 500ms,max-height 500ms}.wcpay-inbox-message-exit{opacity:1;max-height:100vh;transform:translateX(0%)}.wcpay-inbox-message-exit-active{opacity:0;max-height:0;transform:translateX(50%);transition:opacity 500ms,transform 500ms,max-height 500ms} .wcpay-update-business-details-modal{max-width:700px !important}@media(max-width: 600px){.wcpay-update-business-details-modal.components-modal__frame{max-width:700px}}.wcpay-update-business-details-modal .components-notice{margin:16px 0 0}.wcpay-update-business-details-modal hr{margin:24px -32px 0}.wcpay-update-business-details-modal .wcpay-update-business-details-modal__footer{display:flex;justify-content:flex-end;margin-top:24px}.wcpay-update-business-details-modal .wcpay-update-business-details-modal__footer>*:not(:first-child){margin-left:16px} /*!**************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\ !*** css ../../../node_modules/.pnpm/css-loader@3.6.0_webpack@5.89.0_webpack-cli@3.3.12_/node_modules/css-loader/dist/cjs.js!../../../node_modules/.pnpm/postcss-loader@4.3.0_postcss@8.4.32_webpack@5.89.0_webpack-cli@3.3.12_/node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[2]!../../../node_modules/.pnpm/sass-loader@10.5.0_sass@1.69.5_webpack@5.89.0_webpack-cli@3.3.12_/node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[0].use[3]!./src/style.scss ***! \**************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/ @charset "UTF-8"; /** * Colors */ /** * Colors */ /** * SCSS Variables. * * Please use variables from this sheet to ensure consistency across the UI. * Don't add to this sheet unless you're pretty sure the value will be reused in many places. * For example, don't add rules to this sheet that affect block visuals. It's purely for UI. */ /** * Colors */ /** * Fonts & basic variables. */ /** * Grid System. * https://make.wordpress.org/design/2019/10/31/proposal-a-consistent-spacing-system-for-wordpress/ */ /** * Dimensions. */ /** * Shadows. */ /** * Editor widths. */ /** * Block & Editor UI. */ /** * Block paddings. */ /** * React Native specific. * These variables do not appear to be used anywhere else. */ /** * Converts a hex value into the rgb equivalent. * * @param {string} hex - the hexadecimal value to convert * @return {string} comma separated rgb values */ /** * Breakpoint mixins */ /** * Long content fade mixin * * Creates a fading overlay to signify that the content is longer * than the space allows. */ /** * Focus styles. */ /** * Applies editor left position to the selector passed as argument */ /** * Styles that are reused verbatim in a few places */ /** * Allows users to opt-out of animations via OS-level preferences. */ /** * Reset default styles for JavaScript UI based pages. * This is a WP-admin agnostic reset */ /** * Reset the WP Admin page styles for Gutenberg-like pages. */ /** * Breakpoints & Media Queries */ :root { --wp-admin-theme-color: #007cba; --wp-admin-theme-color--rgb: 0, 124, 186; --wp-admin-theme-color-darker-10: #006ba1; --wp-admin-theme-color-darker-10--rgb: 0, 107, 161; --wp-admin-theme-color-darker-20: #005a87; --wp-admin-theme-color-darker-20--rgb: 0, 90, 135; --wp-admin-border-width-focus: 2px; } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { :root { --wp-admin-border-width-focus: 1.5px; } } /* stylelint-disable block-closing-brace-newline-after */ /* stylelint-enable */ /** * Breakpoint mixins */ /* stylelint-disable block-closing-brace-newline-after */ /* stylelint-enable */ /** * Internal Dependencies */ .woocommerce-experimental-list { margin: 0; padding: 0; } a.woocommerce-experimental-list__item { color: inherit; } .woocommerce-experimental-list__item { display: flex; align-items: center; margin-bottom: 0; text-decoration: none; background-color: #fff; } .woocommerce-experimental-list__item.has-gutters { padding: 16px 24px; } .woocommerce-experimental-list__item.has-action:not(.expanded) { cursor: pointer; } .woocommerce-experimental-list__item:focus:not(.expanded) { box-shadow: inset 0 0 0 1px #006088, inset 0 0 0 2px #fff; } .woocommerce-experimental-list__item:focus-visible { box-shadow: none; } .woocommerce-experimental-list__item:not(.transitions-disabled).woocommerce-list__item-enter { opacity: 0; max-height: 0; transform: translateX(50%); } .woocommerce-experimental-list__item:not(.transitions-disabled).woocommerce-list__item-enter-active { opacity: 1; max-height: 100vh; transform: translateX(0%); transition: opacity 500ms, transform 500ms, max-height 500ms; } .woocommerce-experimental-list__item:not(.transitions-disabled).woocommerce-list__item-exit { opacity: 1; max-height: 100vh; transform: translateX(0%); } .woocommerce-experimental-list__item:not(.transitions-disabled).woocommerce-list__item-exit-active { opacity: 0; max-height: 0; transform: translateX(50%); transition: opacity 500ms, transform 500ms, max-height 500ms; } .woocommerce-experimental-list__item > .woocommerce-list__item-inner { text-decoration: none; width: 100%; display: flex; align-items: center; padding: 16px 24px; } .woocommerce-experimental-list__item > .woocommerce-list__item-inner:focus { box-shadow: inset 0 0 0 1px #006088, inset 0 0 0 2px #fff; } .woocommerce-experimental-list__item .woocommerce-list__item-title { color: #1d2327; } .woocommerce-experimental-list__item .woocommerce-list__item-expandable-content { margin-top: 4px; display: block; font-size: 14px; line-height: 20px; color: #50575d; } .woocommerce-experimental-list__item .woocommerce-list__item-before { margin-right: 20px; display: flex; align-items: center; } .woocommerce-experimental-list__item .woocommerce-list__item-after { margin-left: 16px; display: flex; align-items: center; margin-left: auto; } .woocommerce-experimental-list__item:not(:first-child) { border-top: 1px solid #f0f0f0; } .woocommerce-experimental-list__item:hover { background-color: #f0f0f0; } .woocommerce-experimental-list__item:hover .woocommerce-list__item-title { color: var(--wp-admin-theme-color); } .woocommerce-experimental-list__item:hover .woocommerce-task-list__item-badge { background-color: #fff; } .woocommerce-experimental-list__item:hover .woocommerce-list__item-before > svg { fill: var(--wp-admin-theme-color); } .woocommerce-experimental-list__item .woocommerce-list__item-title { color: var(--wp-admin-theme-color); } .woocommerce-experimental-list__item .woocommerce-list__item-before > svg { fill: var(--wp-admin-theme-color); } .woocommerce-experimental-list__item .woocommerce-list__item-after > svg { fill: #1e1e1e; } .woocommerce-experimental-list__item.complete .woocommerce-task__icon { background-color: var(--wp-admin-theme-color); } .woocommerce-experimental-list__item.complete .woocommerce-list__item-title { color: #757575; } .woocommerce-experimental-list__item.complete .woocommerce-list__item-expandable-content { display: none; } .woocommerce-experimental-list__item-title { color: #2c3338; } .woocommerce-experimental-list__item-expandable-content { color: #646970; } .woocommerce-experimental-list__item.list-item-collapse { overflow: hidden; display: grid; grid-template-columns: auto 48px; } .woocommerce-experimental-list__item.list-item-collapse > * { align-self: center; } .woocommerce-experimental-list__item.list-item-collapse p { margin: 0; padding: 16px 24px; } .woocommerce-experimental-list__item.list-item-collapse .list-item-collapse__icon-container { justify-content: flex-end; } .woocommerce-experimental-list__item.list-item-collapse.woocommerce-list__item-enter { max-height: 0; opacity: 1; } .woocommerce-experimental-list__item.list-item-collapse.woocommerce-list__item-enter-active { max-height: 55px; transition: max-height 500ms; } .woocommerce-experimental-list__item.list-item-collapse.woocommerce-list__item-exit { max-height: 55px; } .woocommerce-experimental-list__item.list-item-collapse.woocommerce-list__item-exit-active { max-height: 0; opacity: 1; transition: max-height 500ms; } .woocommerce-experimental-list-wrapper { border-top: 1px solid #f0f0f0; } .woocommerce-task-list__item { position: relative; display: grid; grid-template-columns: 72px auto 48px; } .woocommerce-task-list__item > * { align-self: center; } .woocommerce-task-list__item::before { content: ""; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: transparent; } .woocommerce-task-list__item.level-1::before { background-color: #d94f4f; } .woocommerce-task-list__item.level-1 .gridicons-notice-outline { fill: #d94f4f; } .woocommerce-task-list__item.level-2::before { background-color: #f0b849; } .woocommerce-task-list__item .woocommerce-task-list__item-title { color: var(--wp-admin-theme-color); display: flex; -moz-column-gap: 12px; column-gap: 12px; row-gap: 4px; flex-wrap: wrap; } .woocommerce-task-list__item .woocommerce-task-list__item-badge { padding: 0 10px; background-color: #f6f7f7; border-radius: 2px; color: #2f2f2f; font-size: 11px; font-weight: 500; line-height: 20px; } .woocommerce-task-list__item .woocommerce-task__additional-info, .woocommerce-task-list__item .woocommerce-task-list__item-expandable-content, .woocommerce-task-list__item .woocommerce-task__estimated-time { color: #757575; font-weight: 400; font-size: 12px; } .woocommerce-task-list__item .woocommerce-task__estimated-time { margin-top: 4px; } .woocommerce-task-list__item .woocommerce-task-list__item-before { display: flex; align-items: center; padding: 16px 0 16px 24px; } .woocommerce-task-list__item .woocommerce-task-list__item-text { padding: 16px 0; } .woocommerce-task-list__item .woocommerce-task-list__item-text .woocommerce-pill { padding: 1px 8px; margin-left: 8px; } .woocommerce-task-list__item .woocommerce-task-list__item-expandable-content { margin-top: 4px; overflow: hidden; } .woocommerce-task-list__item .woocommerce-task-list__item-expandable-content.woocommerce-task-list__item-expandable-content-enter { opacity: 0; } .woocommerce-task-list__item .woocommerce-task-list__item-expandable-content.woocommerce-task-list__item-expandable-content-enter-active { opacity: 1; } .woocommerce-task-list__item .woocommerce-task-list__item-expandable-content.woocommerce-task-list__item-expandable-content-enter-done { opacity: 1; } .woocommerce-task-list__item .woocommerce-task-list__item-expandable-content.woocommerce-task-list__item-expandable-content-exit { opacity: 1; } .woocommerce-task-list__item .woocommerce-task-list__item-expandable-content.woocommerce-task-list__item-expandable-content-exit-active { opacity: 0; } .woocommerce-task-list__item .woocommerce-task-list__item-expandable-content .woocommerce-task__additional-info { margin-top: 8px; } .woocommerce-task-list__item .woocommerce-task-list__item-action { margin-top: 8px; margin-bottom: 4px; display: block; } .woocommerce-task-list__item .woocommerce-task-list__item-after { display: flex; align-items: center; } .woocommerce-task-list__item .woocommerce-task-list__item-before .woocommerce-task__icon { border-radius: 50%; width: 32px; height: 32px; } .woocommerce-task-list__item .woocommerce-task-list__item-before .woocommerce-task__icon svg { fill: #fff; position: relative; top: 4px; left: 5px; } .woocommerce-task-list__item.complete .woocommerce-task__icon { background-color: var(--wp-admin-theme-color); } .woocommerce-task-list__item.complete .woocommerce-task-list__item-title { color: #757575; } .woocommerce-task-list__item.complete .woocommerce-task-list__item-expandable-content, .woocommerce-task-list__item.complete .woocommerce-task__estimated-time { display: none; } .woocommerce-task-list__item:not(.complete) .woocommerce-task__icon { border: 1px solid #f0f0f0; background: #fff; } .woocommerce-task-list__item .components-tooltip .components-popover__content { width: 160px; white-space: normal; } .woocommerce-task-list__item .woocommerce-task-card__section-controls { text-align: left; } .woocommerce-inbox-message { position: relative; color: #757575; background: #fff; border-radius: 2px; font-size: 13px; font-size: 0.8125rem; margin: 0 0; -ms-box-orient: horizontal; } .woocommerce-inbox-message.banner { flex-direction: column; } .woocommerce-inbox-message.banner img { width: 100%; } .woocommerce-inbox-message.thumbnail { display: flex; flex-direction: row-reverse; } .woocommerce-inbox-message.thumbnail img { width: 128px; height: 100%; } .woocommerce-inbox-message:hover { background: #f0f0f0; } .woocommerce-inbox-message:not(.message-is-unread) .woocommerce-inbox-message__title, .woocommerce-inbox-message:not(.message-is-unread) .woocommerce-inbox-message__content .woocommerce-inbox-message__title a, .woocommerce-inbox-message__content .woocommerce-inbox-message__title .woocommerce-inbox-message:not(.message-is-unread) a { font-weight: normal; } .woocommerce-inbox-message:not(.message-is-unread) .woocommerce-inbox-message__title a, .woocommerce-inbox-message__content .woocommerce-inbox-message__title .woocommerce-inbox-message:not(.message-is-unread) a a { font-weight: normal; } .woocommerce-homepage-column .woocommerce-inbox-message { margin: 20px 0; } .woocommerce-inbox-message:not(.is-placeholder) { border: 0; border-bottom: 1px solid #e0e0e0; } .woocommerce-inbox-message .line { width: 100%; } .woocommerce-inbox-message .third-line { width: 33%; } .woocommerce-inbox-message .fifth-line { width: 20%; } .woocommerce-inbox-message .sixth-line { width: 16%; } .woocommerce-inbox-message__content .woocommerce-inbox-message__title, .woocommerce-inbox-message__content .woocommerce-inbox-message__title a { color: #1e1e1e; font-size: 16px; font-size: 1rem; font-style: normal; line-height: 1.5; font-weight: bold; margin: 8px 0; } .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__title, .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__title a { margin-bottom: 10px; } .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__title > div, .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__title a > div { animation: loading-fade 1.6s ease-in-out infinite; background-color: #f0f0f0; color: transparent; margin: 5px 0; } .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__title > div::after, .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__title a > div::after { content: " "; } @media screen and (prefers-reduced-motion: reduce) { .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__title > div, .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__title a > div { animation: none; } } .woocommerce-inbox-message__content .woocommerce-inbox-message__title a { color: #1e1e1e !important; text-decoration: none !important; } .woocommerce-inbox-message__content .woocommerce-inbox-message__date { color: #757575; font-size: 12px; font-size: 0.75rem; margin-bottom: 16px; font-style: normal; font-weight: normal; line-height: 16px; } .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__date { margin-bottom: 10px; } .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__date > div { animation: loading-fade 1.6s ease-in-out infinite; background-color: #f0f0f0; color: transparent; } .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__date > div::after { content: " "; } @media screen and (prefers-reduced-motion: reduce) { .is-placeholder .woocommerce-inbox-message__content .woocommerce-inbox-message__date > div { animation: none; } } .woocommerce-inbox-message__wrapper .woocommerce-inbox-message__content { padding-bottom: 0; } .woocommerce-inbox-message__text { color: #757575; font-style: normal; font-weight: normal; font-size: 14px; font-size: 0.875rem; line-height: 20px; } .woocommerce-inbox-message__text > p:first-child { margin-top: 0; } .woocommerce-inbox-message__text > p:last-child { margin-bottom: 0; } .is-placeholder .woocommerce-inbox-message__text > div { animation: loading-fade 1.6s ease-in-out infinite; background-color: #f0f0f0; color: transparent; margin: 5px 0; } .is-placeholder .woocommerce-inbox-message__text > div::after { content: " "; } @media screen and (prefers-reduced-motion: reduce) { .is-placeholder .woocommerce-inbox-message__text > div { animation: none; } } .woocommerce-inbox-message__actions { border-top: 0; } .woocommerce-inbox-message__actions > * + * { margin-left: 0.5em; } .woocommerce-inbox-message__actions a, .woocommerce-inbox-message__actions button { cursor: pointer; } .woocommerce-inbox-message__actions a.is-link, .woocommerce-inbox-message__actions button.is-link { text-decoration: none; } .woocommerce-inbox-message__actions .woocommerce-inbox-note__action-button { max-width: calc(100% - 80px); overflow: hidden; } .woocommerce-inbox-message__actions .woocommerce-inbox-note__action-button span { display: inline-block; overflow: hidden; text-overflow: ellipsis; } .woocommerce-inbox-message__actions.has-multiple-actions button.is-link { padding: 6px 12px; border: 1px solid var(--wp-admin-theme-color); margin-right: 0.5em; } .woocommerce-inbox-message__actions.has-multiple-actions a { margin-right: 0.5em; } .woocommerce-inbox-message__actions button.woocommerce-admin-dismiss-notification { color: #757575; visibility: visible; } .woocommerce-inbox-message__actions button.woocommerce-admin-dismiss-notification:hover { box-shadow: inset 0 0 0 1px #757575; } .woocommerce-inbox-message__actions .components-dropdown { display: inline; } .woocommerce-inbox-message__actions .components-dropdown .components-popover__content { min-width: 195px; } .woocommerce-inbox-message__actions .components-dropdown .components-popover__content ul { text-align: center; } .woocommerce-inbox-message__actions .components-dropdown .components-popover__content li { margin: 0; cursor: pointer; } .is-placeholder .woocommerce-inbox-message__actions > div { animation: loading-fade 1.6s ease-in-out infinite; background-color: #f0f0f0; color: transparent; float: left; height: 28px; margin-right: 8px; } .is-placeholder .woocommerce-inbox-message__actions > div::after { content: " "; } @media screen and (prefers-reduced-motion: reduce) { .is-placeholder .woocommerce-inbox-message__actions > div { animation: none; } } .woocommerce-inbox-message__wrapper { padding-top: 0; } .woocommerce-inbox-dismiss-confirmation_modal { text-align: left; } .woocommerce-inbox-dismiss-confirmation_wrapper p { font-size: 16px; color: #757575; } .woocommerce-inbox-dismiss-confirmation_wrapper .woocommerce-inbox-dismiss-confirmation_buttons { text-align: right; } .woocommerce-inbox-dismiss-confirmation_wrapper .woocommerce-inbox-dismiss-confirmation_buttons button { margin-left: 10px; } .woocommerce-inbox-message__wrapper > div { padding: 16px 24px; } .is-placeholder .woocommerce-inbox-message__wrapper > div { padding: 10px 24px; display: flow-root; } @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { .woocommerce-admin-dismiss-dropdown { margin-top: 0; } .is-placeholder .woocommerce-inbox-message__wrapper { padding-bottom: 10px; } } .woocommerce-inbox-message__wrapper .woocommerce-inbox-message__content{padding-bottom:24px}.woocommerce-inbox-message__wrapper .woocommerce-inbox-message__actions{padding-top:16px}.wcpay-task-list.woocommerce-experimental-list{position:relative}.wcpay-task-list.woocommerce-experimental-list .woocommerce-experimental-list__item{position:relative}.wcpay-task-list.woocommerce-experimental-list .woocommerce-task-list__item[data-key^=dispute-resolution-task] .components-popover{display:none}.wcpay-task-list.woocommerce-experimental-list .woocommerce-task-list__item[data-key^=dispute-resolution-task]:not([data-urgent=true]) .gridicons-notice-outline{fill:#f0b849}.wcpay-task-list.woocommerce-experimental-list .woocommerce-task-list__item[data-key^=dispute-resolution-task]:not([data-urgent=true])::before{background-color:#f0b849}.wcpay-overview>.components-notice{margin:24px 0}.wcpay-overview .components-popover{position:fixed}.wcpay-overview .stripe-notifications-banner-wrapper{padding-bottom:.25rem;overflow:hidden;margin-bottom:24px;margin-left:-1px;margin-right:-1px}.wcpay-overview .stripe-notifications-banner-loader{width:min(680px,90vw);height:160px;display:flex;justify-content:center;align-items:center}.wcpay-overview .wcpay-setup-real-payments__body{display:grid;grid-template-columns:repeat(3, 1fr);grid-column-gap:40px;margin:16px 12px;text-align:center;fill:#720eec}@media(max-width: 783px){.wcpay-overview .wcpay-setup-real-payments__body{grid-template-columns:1fr}}.wcpay-overview .wcpay-setup-real-payments__body svg{display:block;margin:0 auto 8px}.wcpay-overview .wcpay-setup-real-payments__footer{justify-content:end} @keyframes SpinnerAnimationShow{0%{opacity:0}100%{opacity:1}}@keyframes SpinnerAnimationRotation{0%{transform:scaleX(-1) rotateZ(0deg)}50%{transform:scaleX(-1) rotateZ(-180deg)}100%{transform:scaleX(-1) rotateZ(-360deg)}}.stripe-spinner{animation:SpinnerAnimationShow 250ms ease,SpinnerAnimationRotation .7s linear infinite;transition-timing-function:ease;transition-property:opacity,transform;transform-origin:50% 50%;display:inline-block;vertical-align:middle} .components-tooltip{background:#000;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;border-radius:2px;color:#f0f0f0;text-align:center;line-height:1.4;font-size:12px;padding:4px 8px;z-index:1000002;box-shadow:0 1px 2px rgba(0,0,0,.05),0 2px 3px rgba(0,0,0,.04),0 6px 6px rgba(0,0,0,.03),0 8px 8px rgba(0,0,0,.02)}.components-tooltip__shortcut{margin-left:8px}.documents-list .date-time{min-width:195px}.documents-list .woocommerce-table__header.is-numeric .components-button{margin-right:12px}.documents-list .woocommerce-table__header.is-center-aligned{text-align:center}button.components-button.documents-list__download-button{padding:3px}.woocommerce-report-table .woocommerce-search{flex-grow:1}.woocommerce-report-table .woocommerce-card__header{position:relative}.woocommerce-report-table.has-compare .woocommerce-card__action,.woocommerce-report-table.has-search .woocommerce-card__action{align-items:center;text-align:left;display:grid;width:100%;grid-template-columns:auto 1fr auto}@media(max-width: 960px){.woocommerce-report-table.has-compare .woocommerce-card__action,.woocommerce-report-table.has-search .woocommerce-card__action{grid-gap:12px;grid-template-columns:auto 1fr;grid-row-start:2;grid-row-end:2;grid-column-start:1;grid-column-end:4;margin:0}.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-table__compare,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-table__compare{display:flex}.woocommerce-report-table.has-compare .woocommerce-card__action .woocommerce-search,.woocommerce-report-table.has-search .woocommerce-card__action .woocommerce-search{grid-area:2/2/3/4;margin-right:0}}.woocommerce-report-table.has-compare.has-search:not(.has-compare) .woocommerce-card__action,.woocommerce-report-table.has-search.has-search:not(.has-compare) .woocommerce-card__action{grid-template-columns:1fr auto}.woocommerce-report-table.has-compare.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-search,.woocommerce-report-table.has-search.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-search{align-self:center;grid-column-start:1;grid-column-end:2}@media(max-width: 960px){.woocommerce-report-table.has-compare.has-search:not(.has-compare) .woocommerce-card__action,.woocommerce-report-table.has-search.has-search:not(.has-compare) .woocommerce-card__action{grid-template-columns:auto}.woocommerce-report-table.has-compare.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-search,.woocommerce-report-table.has-search.has-search:not(.has-compare) .woocommerce-card__action .woocommerce-search{grid-area:2/1/3/4;margin-left:0}}.woocommerce-report-table.has-compare .woocommerce-search,.woocommerce-report-table.has-search .woocommerce-search{margin:0 16px}.woocommerce-report-table.has-compare .woocommerce-search .woocommerce-select-control__control,.woocommerce-report-table.has-search .woocommerce-search .woocommerce-select-control__control{height:38px}.woocommerce-report-table.has-compare .woocommerce-compare-button,.woocommerce-report-table.has-search .woocommerce-compare-button{padding:3px 12px;height:auto} .components-notice.vat-number-error{margin:0;margin-bottom:15px}.wcpay-vat-number-task__checkbox{margin-bottom:16px}.wcpay-vat-number-task__text-control{margin-bottom:16px}.wcpay-company-data-task__text-control{margin-bottom:16px}.wcpay-company-data-task__textarea-control{margin-bottom:16px} .wcpay.components-custom-select-control{font-size:13px;color:#1e1e1e;position:relative}.wcpay.components-custom-select-control .components-custom-select-control__label{display:inline-block;margin-bottom:8px}.wcpay.components-custom-select-control .components-custom-select-control__item{padding:12px;margin:0;line-height:initial;box-shadow:inset 0 1px 0 #f0f0f0;align-items:center;cursor:default;display:grid;grid-template-columns:auto auto;list-style-type:none}.wcpay.components-custom-select-control .components-custom-select-control__item.is-highlighted{background:#f6f7f7}.wcpay.components-custom-select-control .components-custom-select-control__item .components-custom-select-control__item-icon{margin-left:auto}.wcpay.components-custom-select-control button.components-custom-select-control__button{width:100%;background-color:#fff;margin:0 1px;display:flex;justify-content:space-between;align-items:center;padding:6px 8px;border:1px solid #949494}.wcpay.components-custom-select-control button.components-custom-select-control__button:hover{color:initial}.wcpay.components-custom-select-control button.components-custom-select-control__button.placeholder{color:#646970}.wcpay.components-custom-select-control button.components-custom-select-control__button .components-custom-select-control__button-value{text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden}.wcpay.components-custom-select-control button.components-custom-select-control__button svg{fill:initial;width:18px;flex:0 0 18px}@media screen and (max-width: 782px){.wcpay.components-custom-select-control button.components-custom-select-control__button{font-size:16px}}.wcpay.components-custom-select-control .components-custom-select-control__menu[aria-hidden=true]{display:none}.wcpay.components-custom-select-control .components-custom-select-control__menu{margin:-1px 1px;background-color:#fff;border:1px solid #ddd;border-radius:2px;max-height:300px;min-width:100%;outline:none;overflow:auto;padding:0;position:absolute;transition:none;z-index:1000000} .wcpay-component-grouped-select-control{position:relative}.wcpay-component-grouped-select-control__label{display:inline-block;margin-bottom:8px}.wcpay-component-grouped-select-control__button{min-height:36px;background-color:#fff;color:#1e1e1e;margin:0 1px;text-align:initial;display:flex;justify-content:space-between;align-items:center}.wcpay-component-grouped-select-control__button-value{text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden}.wcpay-component-grouped-select-control__button-icon{flex:0 0 18px}.wcpay-component-grouped-select-control__button.placeholder{color:#646970}@media screen and (max-width: 782px){.wcpay-component-grouped-select-control__button{font-size:16px}}.wcpay-component-grouped-select-control .wcpay-component-grouped-select-control__search{min-height:36px;width:100%;margin:0;border:0;padding:8px 12px;border-radius:0;border-bottom:1px solid #ddd}.wcpay-component-grouped-select-control .wcpay-component-grouped-select-control__search:focus{box-shadow:none;border-color:#ddd}.wcpay-component-grouped-select-control__list{position:absolute;max-height:300px;min-width:100%;background-color:#fff;border-radius:2px;border:1px solid #ddd;margin:16px 1px;z-index:10000;display:flex;flex-direction:column}.wcpay-component-grouped-select-control__list[aria-hidden=true]{display:none}.wcpay-component-grouped-select-control__list-container{overflow:auto}.wcpay-component-grouped-select-control__item{cursor:default;display:grid;grid-template-columns:auto 24px;align-items:center;padding:12px;margin:0;min-height:48px}.wcpay-component-grouped-select-control__item.is-highlighted{background:#f6f7f7}.wcpay-component-grouped-select-control__item.is-item .wcpay-component-grouped-select-control__item-content{padding-left:16px}.wcpay-component-grouped-select-control__item.is-group{color:#757575}.wcpay-component-grouped-select-control__list{margin:-1px 1px}.wcpay-component-grouped-select-control .wcpay-component-grouped-select-control__search,.wcpay-component-grouped-select-control__list{font-size:13px}@media screen and (max-width: 782px){.wcpay-component-grouped-select-control .wcpay-component-grouped-select-control__search,.wcpay-component-grouped-select-control__list{font-size:16px}} .components-base-control.has-error .components-text-control__input{border-color:#cc1818}.components-custom-select-control.has-error,.wcpay-component-grouped-select-control.has-error{margin-bottom:8px}.components-custom-select-control.has-error button,.wcpay-component-grouped-select-control.has-error button{border-color:#cc1818 !important}.components-form-field__error{margin-top:-4px;margin-bottom:8px;font-size:12px;color:#cc1818} body.wcpay-onboarding__body{background-color:#fff}body.wcpay-onboarding__body .wcpay-onboarding-mox a{text-decoration:none}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__nav{position:fixed;top:0;left:0;right:0;height:80px;padding-top:8px;padding-left:8px;padding-right:8px;display:grid;grid-template-columns:94px 1fr;align-items:stretch;background-color:#fff;border-bottom:1px solid #ddd;z-index:10}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__nav-button{color:var(--wp-components-color-accent, #3858e9);cursor:pointer;background-color:transparent;border:none;display:flex;align-items:center;padding:24px;font-size:14px}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__nav-button .gridicons-chevron-left{fill:var(--wp-components-color-accent, #3858e9);margin-right:2px}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__nav-button:last-child{justify-self:end}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__nav-button.hide{visibility:hidden}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__nav-logo{justify-self:center;align-self:center;height:17px}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__wrapper{max-width:620px;margin:116px auto 0;display:flex;flex-direction:column;align-items:center}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__heading{font-size:32px;line-height:40px;font-weight:400;font-family:"SF Pro Display",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;color:#101517;text-align:center}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__subheading{font-size:16px;line-height:24px;font-weight:400;text-align:center;text-wrap:wrap;color:#50575e;margin:12px 0 40px}@media screen and (max-width: 600px){body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__subheading{text-wrap:balance}}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__content{max-width:615px;width:100%}@media screen and (max-width: 480px){body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__content{width:100%;padding:0 16px}}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__cta{display:block;width:100%;height:40px;margin-top:24px;background:var(--wp-components-color-accent, #3858e9)}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__cta:hover{background:var(--wp-components-color-accent, #1d35b4)}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__cta.inline{width:auto;margin-top:0}body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__cta.components-button.is-primary.is-busy,body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__cta.components-button.is-primary.is-busy:disabled,body.wcpay-onboarding__body .wcpay-onboarding-mox .stepper__cta.components-button.is-primary.is-busy[aria-disabled=true]{background:linear-gradient(-45deg, var(--wp-components-color-accent, #3858e9) 33%, var(--wp-components-color-accent-darker-20, #1d35b4) 33%, var(--wp-components-color-accent-darker-20, #1d35b4) 70%, var(--wp-components-color-accent, var(--wp-components-color-accent, #3858e9)) 70%);background-size:100px 100%}body.wcpay-onboarding__body .wcpay-onboarding-mox .wcpay-inline-notice{background-color:#f6f7f7}body.wcpay-onboarding__body .wcpay-onboarding-mox .wcpay-inline-notice__content{display:flex;width:100%}body.wcpay-onboarding__body .wcpay-onboarding-mox .wcpay-inline-notice__content__title{width:inherit}body.wcpay-onboarding__body .wcpay-onboarding-mox .wcpay-inline-notice__content__actions{width:auto;padding-top:0}body.wcpay-onboarding__body .wcpay-onboarding-mox .wcpay-banner-notice--embedded-kyc.is-warning{background-color:#fef8ee}body.wcpay-onboarding__body .wcpay-onboarding-mox .wcpay-banner-notice--embedded-kyc.is-error{background-color:#f4a2a2}body.wcpay-onboarding__body .wcpay-onboarding-mox .wcpay-onboarding__tos{font-size:12px}body.wcpay-onboarding__body .wcpay-onboarding-mox .wcpay-component-grouped-select-control__button-value{color:#1e1e1e}body.wcpay-onboarding__body .wcpay-onboarding-mox .complete-business-info-task__option-description{font-size:12px;color:#757575;line-height:16px;margin-top:4px}body.wcpay-onboarding__body .wcpay-onboarding-mox .onboarding-mode__note{background-color:#f0f6fc;padding:12px 16px}body.wcpay-onboarding__body .wcpay-onboarding-mox .onboarding-mode__sandbox{display:flex;align-items:center;justify-content:center}body.wcpay-onboarding__body .wcpay-onboarding-mox .personal-details-notice{margin:0}body.wcpay-onboarding__body .wcpay-onboarding-mox .components-base-control,body.wcpay-onboarding__body .wcpay-onboarding-mox .components-custom-select-control,body.wcpay-onboarding__body .wcpay-onboarding-mox .wcpay-component-grouped-select-control{margin-bottom:24px}body.wcpay-onboarding__body .wcpay-onboarding-mox .components-form-field__error{margin:-16px 0 16px}body.wcpay-onboarding__body .wcpay-component-onboarding-card{display:block;padding:24px;background-color:#fff;border-radius:3px;border:1px solid #e2e4e7;margin-bottom:24px}body.wcpay-onboarding__body .wcpay-component-onboarding-card__label{display:grid;grid-template-columns:24px 1fr 25px;column-gap:12px;align-items:center;font-weight:bold}body.wcpay-onboarding__body .wcpay-component-onboarding-card__label:not(:last-child){margin-bottom:16px}body.wcpay-onboarding__body .wcpay-component-onboarding-card__label svg{fill:#bbb}body.wcpay-onboarding__body .wcpay-component-onboarding-card__footer button{width:100%;display:flex;align-items:center;justify-content:center}body.wcpay-onboarding__body .wcpay-component-onboarding-card p{color:#757575}body.wcpay-onboarding__body .woocommerce-layout__jitm{display:none}body.wcpay-onboarding__body .embedded-kyc-loader-wrapper{text-align:center;height:35px}body.wcpay-onboarding__body .embedded-kyc-loader-wrapper.padded{padding-top:61px} .fraud-protection-rule-card{margin-bottom:24px} .wcpay-card-body.wcpay-card-body{padding:24px}.wcpay-card-body h4{font-size:var(--m, 13px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);margin-top:0;margin-bottom:24px}.wcpay-card-body h4:not(:first-child){margin-top:32px}.wcpay-card-body h4+p{margin-top:8px;margin-bottom:24px;font-size:12px;font-style:normal;color:#757575}.wcpay-card-body h4:has(+p){margin-bottom:0}.wcpay-card-body>*{margin-top:0;margin-bottom:1em}.wcpay-card-body>*:last-child{margin-bottom:0}.wcpay-card-body>*:last-child>:last-child{margin-bottom:0}.wcpay-card-body input,.wcpay-card-body select{margin:0}.wcpay-card-body ul>li:last-child{margin-bottom:0}.wcpay-card-body ul>li:last-child .components-base-control__field{margin-bottom:0}.wp-core-ui .wcpay-card-body select{max-width:none}.wcpay-card-body .components-checkbox-control input[type=checkbox]:checked:disabled{opacity:.3} .fraud-protection-rule-description p{color:#757575} .fraud-protection-rule-toggle-children-horizontal-form{display:flex;flex-direction:row;gap:12px;max-width:492px}.fraud-protection-rule-toggle-children-horizontal-form+.fraud-protection-rule-toggle-children-notice{margin-top:13px}.fraud-protection-rule-toggle-children-vertical-form{display:flex;flex:1;flex-direction:column;gap:8px} .wcpay-components-amount-input .components-base-control__field{margin-bottom:0}.wcpay-components-amount-input p{margin-bottom:0}.wcpay-components-amount-input .text-control-with-affixes__prefix{line-height:16px;display:flex;align-items:center;padding:7px 12px} body.woocommerce-admin-page__payments_fraud-protection{background:#fff}body.woocommerce-admin-page__payments_fraud-protection .wcpay-settings-layout{margin:40px 0 0;max-width:1184px}@media screen and (max-width: 782px){body.woocommerce-admin-page__payments_fraud-protection .woocommerce-layout .woocommerce-layout__main{padding-top:0}}body.woocommerce-admin-page__payments_fraud-protection .woocommerce-layout__header{border-bottom:1px solid #ddd}body.woocommerce-admin-page__payments_fraud-protection .woocommerce-layout__header-heading{font-size:16px;padding:0 0 0 30px}body.woocommerce-admin-page__payments_fraud-protection .woocommerce-layout__primary{margin:24px 0 128px 30px}body.woocommerce-admin-page__payments_fraud-protection #woocommerce-activity-panel{display:none}.fraud-protection-advanced-settings-error-notice{margin-bottom:24px}.fraud-protection-advanced-settings__footer{display:flex;justify-content:flex-end}.fraud-protection-advanced-settings__footer>.components-button+.components-button{margin-left:24px}.fraud-protection-header-breadcrumb-legacy small{margin-left:.5em}.fraud-protection-header-breadcrumb small{margin-right:.5em}.fraud-protection-header-breadcrumb a{color:#1d2327;text-decoration:none}.fraud-protection-header-breadcrumb a:hover{color:var(--wp-admin-theme-color, #3858e9)} dist/woopay-express-button.js.LICENSE.txt 0000777 00000000371 15251156605 0014300 0 ustar 00 /** * @license React * react-jsx-runtime.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ dist/multi-currency-switcher-block.js.LICENSE.txt 0000777 00000000371 15251156605 0015662 0 ustar 00 /** * @license React * react-jsx-runtime.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ dist/express-checkout.js 0000777 00000154776 15251156605 0011402 0 ustar 00 (()=>{var e={n:t=>{var o=t&&t.__esModule?()=>t.default:()=>t;return e.d(o,{a:o}),o},d:(t,o)=>{for(var r in o)e.o(o,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:o[r]})}};e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var t;e.g.importScripts&&(t=e.g.location+"");var o=e.g.document;if(!t&&o&&(o.currentScript&&(t=o.currentScript.src),!t)){var r=o.getElementsByTagName("script");if(r.length)for(var n=r.length-1;n>-1&&(!t||!/^http(s?):/.test(t));)t=r[n--].src}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),e.p=t})(),e.p=window.wcpayAssets.url,(()=>{"use strict";const t=window.wp.i18n,o=window.wp.hooks,r=e=>"undefined"!=typeof wcpayConfig&&void 0!==wcpayConfig[e]?wcpayConfig[e]:n(e),n=e=>{let t=null;if("undefined"!=typeof wcpay_upe_config)t=wcpay_upe_config;else{if("object"!=typeof wc||void 0===wc.wcSettings)return null;t=wc.wcSettings.getSetting("woocommerce_payments_data")||{}}return t[e]||null},a=e=>"object"==typeof wcpayExpressCheckoutParams&&wcpayExpressCheckoutParams.hasOwnProperty(e)?wcpayExpressCheckoutParams[e]:null,i=(e,t,o="wcpay_")=>e.toString().replace("%%endpoint%%",o+t),s=["color","padding","paddingTop","paddingRight","paddingBottom","paddingLeft"],c=["fontFamily","fontSize","lineHeight","letterSpacing","fontWeight","fontVariation","textDecoration","textShadow","textTransform","-webkit-font-smoothing","-moz-osx-font-smoothing","transition"],l=["backgroundColor","border","borderTop","borderRight","borderBottom","borderLeft","borderRadius","borderWidth","borderColor","borderStyle","borderTopWidth","borderTopColor","borderTopStyle","borderRightWidth","borderRightColor","borderRightStyle","borderBottomWidth","borderBottomColor","borderBottomStyle","borderLeftWidth","borderLeftColor","borderLeftStyle","borderTopLeftRadius","borderTopRightRadius","borderBottomRightRadius","borderBottomLeftRadius","outline","outlineOffset","boxShadow"],u={".Label":[...s,...c],".Text":[...s,...c],".Input":[...s,...c,...l],".Error":[...s,...c,...l],".Tab":[...s,...c,...l],".TabIcon":[...s],".TabLabel":[...s,...c],".Block":[...s.slice(1),...l.slice(1)],".Container":[...l],".Header":[...s,...l,...c],".Footer":[...s,...l,...c]},d={".Label":u[".Label"],".Label--floating":[...u[".Label"],"transform"],".Input":[...u[".Input"],"outlineColor","outlineWidth","outlineStyle"],".Error":u[".Error"],".Tab":["backgroundColor","color","fontFamily"],".Tab--selected":["outlineColor","outlineWidth","outlineStyle","backgroundColor","color",...l],".TabIcon":u[".TabIcon"],".TabIcon--selected":["color"],".TabLabel":u[".TabLabel"],".Block":u[".Block"],".Container":u[".Container"],".Header":u[".Header"],".Footer":u[".Footer"],".Footer--link":u[".Text"],".Text":u[".Text"],".Text--redirect":u[".Text"]};function p(e){return p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},p(e)}var h=/^\s+/,m=/\s+$/;function f(e,t){if(t=t||{},(e=e||"")instanceof f)return e;if(!(this instanceof f))return new f(e,t);var o=function(e){var t,o,r,n={r:0,g:0,b:0},a=1,i=null,s=null,c=null,l=!1,u=!1;return"string"==typeof e&&(e=function(e){e=e.replace(h,"").replace(m,"").toLowerCase();var t,o=!1;if(R[e])e=R[e],o=!0;else if("transparent"==e)return{r:0,g:0,b:0,a:0,format:"name"};return(t=U.rgb.exec(e))?{r:t[1],g:t[2],b:t[3]}:(t=U.rgba.exec(e))?{r:t[1],g:t[2],b:t[3],a:t[4]}:(t=U.hsl.exec(e))?{h:t[1],s:t[2],l:t[3]}:(t=U.hsla.exec(e))?{h:t[1],s:t[2],l:t[3],a:t[4]}:(t=U.hsv.exec(e))?{h:t[1],s:t[2],v:t[3]}:(t=U.hsva.exec(e))?{h:t[1],s:t[2],v:t[3],a:t[4]}:(t=U.hex8.exec(e))?{r:B(t[1]),g:B(t[2]),b:B(t[3]),a:O(t[4]),format:o?"name":"hex8"}:(t=U.hex6.exec(e))?{r:B(t[1]),g:B(t[2]),b:B(t[3]),format:o?"name":"hex"}:(t=U.hex4.exec(e))?{r:B(t[1]+""+t[1]),g:B(t[2]+""+t[2]),b:B(t[3]+""+t[3]),a:O(t[4]+""+t[4]),format:o?"name":"hex8"}:!!(t=U.hex3.exec(e))&&{r:B(t[1]+""+t[1]),g:B(t[2]+""+t[2]),b:B(t[3]+""+t[3]),format:o?"name":"hex"}}(e)),"object"==p(e)&&(D(e.r)&&D(e.g)&&D(e.b)?(t=e.r,o=e.g,r=e.b,n={r:255*j(t,255),g:255*j(o,255),b:255*j(r,255)},l=!0,u="%"===String(e.r).substr(-1)?"prgb":"rgb"):D(e.h)&&D(e.s)&&D(e.v)?(i=N(e.s),s=N(e.v),n=function(e,t,o){e=6*j(e,360),t=j(t,100),o=j(o,100);var r=Math.floor(e),n=e-r,a=o*(1-t),i=o*(1-n*t),s=o*(1-(1-n)*t),c=r%6;return{r:255*[o,i,a,a,s,o][c],g:255*[s,o,o,i,a,a][c],b:255*[a,a,s,o,o,i][c]}}(e.h,i,s),l=!0,u="hsv"):D(e.h)&&D(e.s)&&D(e.l)&&(i=N(e.s),c=N(e.l),n=function(e,t,o){var r,n,a;function i(e,t,o){return o<0&&(o+=1),o>1&&(o-=1),o<1/6?e+6*(t-e)*o:o<.5?t:o<2/3?e+(t-e)*(2/3-o)*6:e}if(e=j(e,360),t=j(t,100),o=j(o,100),0===t)r=n=a=o;else{var s=o<.5?o*(1+t):o+t-o*t,c=2*o-s;r=i(c,s,e+1/3),n=i(c,s,e),a=i(c,s,e-1/3)}return{r:255*r,g:255*n,b:255*a}}(e.h,i,c),l=!0,u="hsl"),e.hasOwnProperty("a")&&(a=e.a)),a=L(a),{ok:l,format:e.format||u,r:Math.min(255,Math.max(n.r,0)),g:Math.min(255,Math.max(n.g,0)),b:Math.min(255,Math.max(n.b,0)),a}}(e);this._originalInput=e,this._r=o.r,this._g=o.g,this._b=o.b,this._a=o.a,this._roundA=Math.round(100*this._a)/100,this._format=t.format||o.format,this._gradientType=t.gradientType,this._r<1&&(this._r=Math.round(this._r)),this._g<1&&(this._g=Math.round(this._g)),this._b<1&&(this._b=Math.round(this._b)),this._ok=o.ok}function b(e,t,o){e=j(e,255),t=j(t,255),o=j(o,255);var r,n,a=Math.max(e,t,o),i=Math.min(e,t,o),s=(a+i)/2;if(a==i)r=n=0;else{var c=a-i;switch(n=s>.5?c/(2-a-i):c/(a+i),a){case e:r=(t-o)/c+(t<o?6:0);break;case t:r=(o-e)/c+2;break;case o:r=(e-t)/c+4}r/=6}return{h:r,s:n,l:s}}function g(e,t,o){e=j(e,255),t=j(t,255),o=j(o,255);var r,n,a=Math.max(e,t,o),i=Math.min(e,t,o),s=a,c=a-i;if(n=0===a?0:c/a,a==i)r=0;else{switch(a){case e:r=(t-o)/c+(t<o?6:0);break;case t:r=(o-e)/c+2;break;case o:r=(e-t)/c+4}r/=6}return{h:r,s:n,v:s}}function y(e,t,o,r){var n=[H(Math.round(e).toString(16)),H(Math.round(t).toString(16)),H(Math.round(o).toString(16))];return r&&n[0].charAt(0)==n[0].charAt(1)&&n[1].charAt(0)==n[1].charAt(1)&&n[2].charAt(0)==n[2].charAt(1)?n[0].charAt(0)+n[1].charAt(0)+n[2].charAt(0):n.join("")}function _(e,t,o,r){return[H(z(r)),H(Math.round(e).toString(16)),H(Math.round(t).toString(16)),H(Math.round(o).toString(16))].join("")}function w(e,t){t=0===t?0:t||10;var o=f(e).toHsl();return o.s-=t/100,o.s=F(o.s),f(o)}function v(e,t){t=0===t?0:t||10;var o=f(e).toHsl();return o.s+=t/100,o.s=F(o.s),f(o)}function k(e){return f(e).desaturate(100)}function S(e,t){t=0===t?0:t||10;var o=f(e).toHsl();return o.l+=t/100,o.l=F(o.l),f(o)}function x(e,t){t=0===t?0:t||10;var o=f(e).toRgb();return o.r=Math.max(0,Math.min(255,o.r-Math.round(-t/100*255))),o.g=Math.max(0,Math.min(255,o.g-Math.round(-t/100*255))),o.b=Math.max(0,Math.min(255,o.b-Math.round(-t/100*255))),f(o)}function C(e,t){t=0===t?0:t||10;var o=f(e).toHsl();return o.l-=t/100,o.l=F(o.l),f(o)}function T(e,t){var o=f(e).toHsl(),r=(o.h+t)%360;return o.h=r<0?360+r:r,f(o)}function I(e){var t=f(e).toHsl();return t.h=(t.h+180)%360,f(t)}function A(e,t){if(isNaN(t)||t<=0)throw new Error("Argument to polyad must be a positive number");for(var o=f(e).toHsl(),r=[f(e)],n=360/t,a=1;a<t;a++)r.push(f({h:(o.h+a*n)%360,s:o.s,l:o.l}));return r}function P(e){var t=f(e).toHsl(),o=t.h;return[f(e),f({h:(o+72)%360,s:t.s,l:t.l}),f({h:(o+216)%360,s:t.s,l:t.l})]}function M(e,t,o){t=t||6,o=o||30;var r=f(e).toHsl(),n=360/o,a=[f(e)];for(r.h=(r.h-(n*t>>1)+720)%360;--t;)r.h=(r.h+n)%360,a.push(f(r));return a}function q(e,t){t=t||6;for(var o=f(e).toHsv(),r=o.h,n=o.s,a=o.v,i=[],s=1/t;t--;)i.push(f({h:r,s:n,v:a})),a=(a+s)%1;return i}f.prototype={isDark:function(){return this.getBrightness()<128},isLight:function(){return!this.isDark()},isValid:function(){return this._ok},getOriginalInput:function(){return this._originalInput},getFormat:function(){return this._format},getAlpha:function(){return this._a},getBrightness:function(){var e=this.toRgb();return(299*e.r+587*e.g+114*e.b)/1e3},getLuminance:function(){var e,t,o,r=this.toRgb();return e=r.r/255,t=r.g/255,o=r.b/255,.2126*(e<=.03928?e/12.92:Math.pow((e+.055)/1.055,2.4))+.7152*(t<=.03928?t/12.92:Math.pow((t+.055)/1.055,2.4))+.0722*(o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4))},setAlpha:function(e){return this._a=L(e),this._roundA=Math.round(100*this._a)/100,this},toHsv:function(){var e=g(this._r,this._g,this._b);return{h:360*e.h,s:e.s,v:e.v,a:this._a}},toHsvString:function(){var e=g(this._r,this._g,this._b),t=Math.round(360*e.h),o=Math.round(100*e.s),r=Math.round(100*e.v);return 1==this._a?"hsv("+t+", "+o+"%, "+r+"%)":"hsva("+t+", "+o+"%, "+r+"%, "+this._roundA+")"},toHsl:function(){var e=b(this._r,this._g,this._b);return{h:360*e.h,s:e.s,l:e.l,a:this._a}},toHslString:function(){var e=b(this._r,this._g,this._b),t=Math.round(360*e.h),o=Math.round(100*e.s),r=Math.round(100*e.l);return 1==this._a?"hsl("+t+", "+o+"%, "+r+"%)":"hsla("+t+", "+o+"%, "+r+"%, "+this._roundA+")"},toHex:function(e){return y(this._r,this._g,this._b,e)},toHexString:function(e){return"#"+this.toHex(e)},toHex8:function(e){return function(e,t,o,r,n){var a=[H(Math.round(e).toString(16)),H(Math.round(t).toString(16)),H(Math.round(o).toString(16)),H(z(r))];return n&&a[0].charAt(0)==a[0].charAt(1)&&a[1].charAt(0)==a[1].charAt(1)&&a[2].charAt(0)==a[2].charAt(1)&&a[3].charAt(0)==a[3].charAt(1)?a[0].charAt(0)+a[1].charAt(0)+a[2].charAt(0)+a[3].charAt(0):a.join("")}(this._r,this._g,this._b,this._a,e)},toHex8String:function(e){return"#"+this.toHex8(e)},toRgb:function(){return{r:Math.round(this._r),g:Math.round(this._g),b:Math.round(this._b),a:this._a}},toRgbString:function(){return 1==this._a?"rgb("+Math.round(this._r)+", "+Math.round(this._g)+", "+Math.round(this._b)+")":"rgba("+Math.round(this._r)+", "+Math.round(this._g)+", "+Math.round(this._b)+", "+this._roundA+")"},toPercentageRgb:function(){return{r:Math.round(100*j(this._r,255))+"%",g:Math.round(100*j(this._g,255))+"%",b:Math.round(100*j(this._b,255))+"%",a:this._a}},toPercentageRgbString:function(){return 1==this._a?"rgb("+Math.round(100*j(this._r,255))+"%, "+Math.round(100*j(this._g,255))+"%, "+Math.round(100*j(this._b,255))+"%)":"rgba("+Math.round(100*j(this._r,255))+"%, "+Math.round(100*j(this._g,255))+"%, "+Math.round(100*j(this._b,255))+"%, "+this._roundA+")"},toName:function(){return 0===this._a?"transparent":!(this._a<1)&&(E[y(this._r,this._g,this._b,!0)]||!1)},toFilter:function(e){var t="#"+_(this._r,this._g,this._b,this._a),o=t,r=this._gradientType?"GradientType = 1, ":"";if(e){var n=f(e);o="#"+_(n._r,n._g,n._b,n._a)}return"progid:DXImageTransform.Microsoft.gradient("+r+"startColorstr="+t+",endColorstr="+o+")"},toString:function(e){var t=!!e;e=e||this._format;var o=!1,r=this._a<1&&this._a>=0;return t||!r||"hex"!==e&&"hex6"!==e&&"hex3"!==e&&"hex4"!==e&&"hex8"!==e&&"name"!==e?("rgb"===e&&(o=this.toRgbString()),"prgb"===e&&(o=this.toPercentageRgbString()),"hex"!==e&&"hex6"!==e||(o=this.toHexString()),"hex3"===e&&(o=this.toHexString(!0)),"hex4"===e&&(o=this.toHex8String(!0)),"hex8"===e&&(o=this.toHex8String()),"name"===e&&(o=this.toName()),"hsl"===e&&(o=this.toHslString()),"hsv"===e&&(o=this.toHsvString()),o||this.toHexString()):"name"===e&&0===this._a?this.toName():this.toRgbString()},clone:function(){return f(this.toString())},_applyModification:function(e,t){var o=e.apply(null,[this].concat([].slice.call(t)));return this._r=o._r,this._g=o._g,this._b=o._b,this.setAlpha(o._a),this},lighten:function(){return this._applyModification(S,arguments)},brighten:function(){return this._applyModification(x,arguments)},darken:function(){return this._applyModification(C,arguments)},desaturate:function(){return this._applyModification(w,arguments)},saturate:function(){return this._applyModification(v,arguments)},greyscale:function(){return this._applyModification(k,arguments)},spin:function(){return this._applyModification(T,arguments)},_applyCombination:function(e,t){return e.apply(null,[this].concat([].slice.call(t)))},analogous:function(){return this._applyCombination(M,arguments)},complement:function(){return this._applyCombination(I,arguments)},monochromatic:function(){return this._applyCombination(q,arguments)},splitcomplement:function(){return this._applyCombination(P,arguments)},triad:function(){return this._applyCombination(A,[3])},tetrad:function(){return this._applyCombination(A,[4])}},f.fromRatio=function(e,t){if("object"==p(e)){var o={};for(var r in e)e.hasOwnProperty(r)&&(o[r]="a"===r?e[r]:N(e[r]));e=o}return f(e,t)},f.equals=function(e,t){return!(!e||!t)&&f(e).toRgbString()==f(t).toRgbString()},f.random=function(){return f.fromRatio({r:Math.random(),g:Math.random(),b:Math.random()})},f.mix=function(e,t,o){o=0===o?0:o||50;var r=f(e).toRgb(),n=f(t).toRgb(),a=o/100;return f({r:(n.r-r.r)*a+r.r,g:(n.g-r.g)*a+r.g,b:(n.b-r.b)*a+r.b,a:(n.a-r.a)*a+r.a})},f.readability=function(e,t){var o=f(e),r=f(t);return(Math.max(o.getLuminance(),r.getLuminance())+.05)/(Math.min(o.getLuminance(),r.getLuminance())+.05)},f.isReadable=function(e,t,o){var r,n,a,i,s,c=f.readability(e,t);switch(n=!1,(a=o,"AA"!==(i=((a=a||{level:"AA",size:"small"}).level||"AA").toUpperCase())&&"AAA"!==i&&(i="AA"),"small"!==(s=(a.size||"small").toLowerCase())&&"large"!==s&&(s="small"),r={level:i,size:s}).level+r.size){case"AAsmall":case"AAAlarge":n=c>=4.5;break;case"AAlarge":n=c>=3;break;case"AAAsmall":n=c>=7}return n},f.mostReadable=function(e,t,o){var r,n,a,i,s=null,c=0;n=(o=o||{}).includeFallbackColors,a=o.level,i=o.size;for(var l=0;l<t.length;l++)(r=f.readability(e,t[l]))>c&&(c=r,s=f(t[l]));return f.isReadable(e,s,{level:a,size:i})||!n?s:(o.includeFallbackColors=!1,f.mostReadable(e,["#fff","#000"],o))};var R=f.names={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"0ff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000",blanchedalmond:"ffebcd",blue:"00f",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",burntsienna:"ea7e5d",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"0ff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkgrey:"a9a9a9",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkslategrey:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dimgrey:"696969",dodgerblue:"1e90ff",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"f0f",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",grey:"808080",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgray:"d3d3d3",lightgreen:"90ee90",lightgrey:"d3d3d3",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslategray:"789",lightslategrey:"789",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"0f0",limegreen:"32cd32",linen:"faf0e6",magenta:"f0f",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370db",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"db7093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",rebeccapurple:"663399",red:"f00",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",slategrey:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",wheat:"f5deb3",white:"fff",whitesmoke:"f5f5f5",yellow:"ff0",yellowgreen:"9acd32"},E=f.hexNames=function(e){var t={};for(var o in e)e.hasOwnProperty(o)&&(t[e[o]]=o);return t}(R);function L(e){return e=parseFloat(e),(isNaN(e)||e<0||e>1)&&(e=1),e}function j(e,t){(function(e){return"string"==typeof e&&-1!=e.indexOf(".")&&1===parseFloat(e)})(e)&&(e="100%");var o=function(e){return"string"==typeof e&&-1!=e.indexOf("%")}(e);return e=Math.min(t,Math.max(0,parseFloat(e))),o&&(e=parseInt(e*t,10)/100),Math.abs(e-t)<1e-6?1:e%t/parseFloat(t)}function F(e){return Math.min(1,Math.max(0,e))}function B(e){return parseInt(e,16)}function H(e){return 1==e.length?"0"+e:""+e}function N(e){return e<=1&&(e=100*e+"%"),e}function z(e){return Math.round(255*parseFloat(e)).toString(16)}function O(e){return B(e)/255}var $,W,Q,U=(W="[\\s|\\(]+("+($="(?:[-\\+]?\\d*\\.\\d+%?)|(?:[-\\+]?\\d+%?)")+")[,|\\s]+("+$+")[,|\\s]+("+$+")\\s*\\)?",Q="[\\s|\\(]+("+$+")[,|\\s]+("+$+")[,|\\s]+("+$+")[,|\\s]+("+$+")\\s*\\)?",{CSS_UNIT:new RegExp($),rgb:new RegExp("rgb"+W),rgba:new RegExp("rgba"+Q),hsl:new RegExp("hsl"+W),hsla:new RegExp("hsla"+Q),hsv:new RegExp("hsv"+W),hsva:new RegExp("hsva"+Q),hex3:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex6:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,hex4:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex8:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/});function D(e){return!!U.CSS_UNIT.exec(e)}const V={default:{hiddenContainer:"#wcpay-hidden-div",hiddenInput:"#wcpay-hidden-input",hiddenInvalidInput:"#wcpay-hidden-invalid-input",hiddenValidActiveLabel:"#wcpay-hidden-valid-active-label"},classicCheckout:{appendTarget:".woocommerce-billing-fields__field-wrapper",upeThemeInputSelector:"#billing_first_name",upeThemeLabelSelector:".woocommerce-checkout .form-row label",upeThemeTextSelectors:["#payment .payment_methods li .payment_box fieldset",".woocommerce-checkout .form-row"],rowElement:"p",validClasses:["form-row"],invalidClasses:["form-row","woocommerce-invalid","woocommerce-invalid-required-field"],backgroundSelectors:["li.wc_payment_method .wc-payment-form","li.wc_payment_method .payment_box","#payment","#order_review","form.checkout","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:["#place_order"],linkSelectors:["a"],pmmeRelativeTextSizeSelector:".wc_payment_method > label"},blocksCheckout:{appendTarget:".wc-block-checkout__contact-fields",upeThemeInputSelector:".wc-block-components-text-input #email",upeThemeLabelSelector:".wc-block-components-text-input label",upeThemeTextSelectors:[".wc-block-components-checkout-step__description",".wc-block-components-text-input",".wc-block-components-radio-control__label",".wc-block-checkout__terms"],rowElement:"div",validClasses:["wc-block-components-text-input","is-active"],invalidClasses:["wc-block-components-text-input","has-error"],alternateSelectors:{appendTarget:"#billing.wc-block-components-address-form",upeThemeInputSelector:"#billing-first_name",upeThemeLabelSelector:".wc-block-components-checkout-step__description"},backgroundSelectors:["#payment-method .wc-block-components-radio-control-accordion-option","#payment-method","form.wc-block-checkout__form",".wc-block-checkout","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:[".wc-block-components-checkout-place-order-button"],linkSelectors:["a"],containerSelectors:[".wp-block-woocommerce-checkout-order-summary-block"],pmmeRelativeTextSizeSelector:".wc-block-components-radio-control__label-group"},bnplProductPage:{appendTarget:".product .cart .quantity",upeThemeInputSelector:".product .cart .quantity .qty",upeThemeLabelSelector:".product .cart .quantity label",upeThemeTextSelectors:[".product .cart .quantity"],rowElement:"div",validClasses:["input-text"],invalidClasses:["input-text","has-error"],backgroundSelectors:["#payment-method-message","#main > .product > div.summary.entry-summary","#main > .product","#main","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:[".single_add_to_cart_button"],linkSelectors:["a"]},bnplClassicCart:{appendTarget:".cart .quantity",upeThemeInputSelector:".cart .quantity .qty",upeThemeLabelSelector:".cart .quantity label",upeThemeTextSelectors:[".cart .quantity"],rowElement:"div",validClasses:["input-text"],invalidClasses:["input-text","has-error"],backgroundSelectors:["#payment-method-message","#main .entry-content .cart_totals","#main .entry-content","#main","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:[".checkout-button"],linkSelectors:["a"],containerSelectors:[".shop_table"]},bnplCartBlock:{appendTarget:".wc-block-cart .wc-block-components-quantity-selector",upeThemeInputSelector:".wc-block-cart .wc-block-components-quantity-selector .wc-block-components-quantity-selector__input",upeThemeLabelSelector:".wc-block-components-text-input",upeThemeTextSelectors:[".wc-block-components-text-input"],rowElement:"div",validClasses:["wc-block-components-text-input"],invalidClasses:["wc-block-components-text-input","has-error"],backgroundSelectors:[".wc-block-components-bnpl-wrapper",".wc-block-components-order-meta",".wc-block-components-totals-wrapper",".wp-block-woocommerce-cart-order-summary-block",".wp-block-woocommerce-cart-totals-block",".wp-block-woocommerce-cart .wc-block-cart",".wp-block-woocommerce-cart","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:[".wc-block-cart__submit-button"],linkSelectors:["a"],containerSelectors:[".wp-block-woocommerce-cart-line-items-block"]},wooPayClassicCheckout:{appendTarget:".woocommerce-billing-fields__field-wrapper",upeThemeInputSelector:"#billing_first_name",upeThemeLabelSelector:".woocommerce-checkout .form-row label",upeThemeTextSelectors:[".woocommerce-checkout .form-row"],rowElement:"p",validClasses:["form-row"],invalidClasses:["form-row","woocommerce-invalid","woocommerce-invalid-required-field"],backgroundSelectors:["#customer_details","#order_review","form.checkout","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:["#place_order"],linkSelectors:["a"],containerSelectors:[".woocommerce-checkout-review-order-table"],headerSelectors:[".site-header","header > div"],footerSelectors:[".site-footer","footer > div"],footerLink:[".site-footer a","footer a"]},updateSelectors:function(e,t){return e.hasOwnProperty("alternateSelectors")&&(Object.entries(e.alternateSelectors).forEach((o=>{const[r,n]=o;t.querySelector(e[r])||(e[r]=n)})),delete e.alternateSelectors),e},getSelectors:function(e,t){let o=this.blocksCheckout;switch(e){case"blocks_checkout":o=this.blocksCheckout;break;case"shortcode_checkout":o=this.classicCheckout;break;case"bnpl_product_page":o=this.bnplProductPage;break;case"bnpl_classic_cart":o=this.bnplClassicCart;break;case"bnpl_cart_block":o=this.bnplCartBlock;break;case"woopay_shortcode_checkout":o=this.wooPayClassicCheckout}return{...this.default,...this.updateSelectors(o,t)}}},X={getHiddenContainer:function(e,t){const o=t.createElement("div");return o.setAttribute("id",this.getIDFromSelector(e)),o.style.border=0,o.style.clip="rect(0 0 0 0)",o.style.height="1px",o.style.margin="-1px",o.style.overflow="hidden",o.style.padding="0",o.style.position="absolute",o.style.width="1px",o},createRow:function(e,t=[],o){const r=o.createElement(e);return t.length&&r.classList.add(...t),r},appendClone:function(e,t,o,r){const n=r.querySelector(t);if(n){const t=n.cloneNode(!0);t.id=this.getIDFromSelector(o),t.value="",e.appendChild(t)}},getIDFromSelector:function(e){return e.startsWith("#")||e.startsWith(".")?e.slice(1):e},init:function(e,t){const o=V.getSelectors(e),r=t.querySelector(o.appendTarget),n=t.querySelector(o.upeThemeInputSelector);if(!r||!n)return;t.querySelector(o.hiddenContainer)&&this.cleanup(t);const a=this.getHiddenContainer(o.hiddenContainer,t);r.appendChild(a);const i=this.createRow(o.rowElement,o.validClasses,t);a.appendChild(i);const s=this.createRow(o.rowElement,o.invalidClasses,t);a.appendChild(s),this.appendClone(i,o.upeThemeInputSelector,o.hiddenInput,t),this.appendClone(i,o.upeThemeLabelSelector,o.hiddenValidActiveLabel,t),this.appendClone(s,o.upeThemeInputSelector,o.hiddenInvalidInput,t),this.appendClone(s,o.upeThemeLabelSelector,o.hiddenInvalidInput,t),t.querySelector(o.hiddenInput).style.transition="none"},cleanup:function(e){const t=e.querySelector(V.default.hiddenContainer);t&&t.remove()}},G=(e,t,o=null,r)=>{const n=(Array.isArray(e)?e:[e]).map((e=>r.querySelector(e))).filter(Boolean);if(!n.length)return{};const a=r.defaultView||window,i=d[t],s=n[0],c=a.getComputedStyle(s),l={};if(i.forEach((e=>{const t=e.replace(/[A-Z]/g,(e=>`-${e.toLowerCase()}`)),o=c.getPropertyValue(t);if(o)if("color"!==e)if("lineHeight"!==e||"0"!==o&&"0px"!==o)l[e]=o;else for(let t=1;t<n.length;t++){const o=a.getComputedStyle(n[t]).getPropertyValue("line-height");if("0"!==o&&"0px"!==o){l[e]=o;break}}else l[e]=(e=>{const t=e.match(/^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0?(\.\d+)?|1?(\.0+)?)\s*\)$/);if(t){const o=t[4]||1;e=`rgb(${t.slice(1,4).map((e=>Math.round(e*o+255*(1-o)))).join(", ")})`}return e})(o)})),".Input"===t||".Tab--selected"===t){const e=((e,t="solid",o)=>e&&o?[e,t,o].join(" "):"")(l.outlineWidth,l.outlineStyle,l.outlineColor);""!==e&&(l.outline=e),delete l.outlineWidth,delete l.outlineColor,delete l.outlineStyle}const u=c.getPropertyValue("text-indent");return"0px"!==u&&"0px"===l.paddingLeft&&"0px"===l.paddingRight&&(l.paddingLeft=u,l.paddingRight=u),".Block"===t&&(l.backgroundColor=o),l},J=(e,t=!1,o=document)=>{const r=V.getSelectors(e,o);X.init(e,o);const n=G(r.hiddenInput,".Input",null,o),a=G(r.hiddenInvalidInput,".Input",null,o),i=G(r.upeThemeLabelSelector,".Label",null,o),s={fontSize:i.fontSize},c=G(r.upeThemeTextSelectors,".Text",null,o),l=G(r.upeThemeInputSelector,".Tab",null,o),u=G(r.hiddenInput,".Tab--selected",null,o),d=(e=>{const t=Object.assign({},e);if(!e.backgroundColor||!e.color)return e;const o=((e,t)=>{const o={backgroundColor:e,color:t},r=f(e),n=f(t);if(!r.isValid()||!n.isValid())return{backgroundColor:"",color:""};const a=r.getBrightness()>50?f(r).darken(7):f(r).lighten(7),i=f.mostReadable(a,[n],{includeFallbackColors:!0});return o.backgroundColor=a.toRgbString(),o.color=i.toRgbString(),o})(e.backgroundColor,e.color);return t.backgroundColor=o.backgroundColor,t.color=o.color,t})(l),p={color:d.color},h={color:u.color},m=((e,t=document)=>{let o=null,r=0;for(;!o&&r<e.length;){const n=t.querySelector(e[r]);if(!n){r++;continue}const a=(t.defaultView||window).getComputedStyle(n).backgroundColor;a&&f(a).getAlpha()>0&&(o=a),r++}return o||"#ffffff"})(r.backgroundSelectors,o),b=G(r.headingSelectors,".Label",null,o),g=G(r.upeThemeLabelSelector,".Block",m,o),y=G(r.buttonSelectors,".Input",null,o),_=G(r.linkSelectors,".Label",null,o),w=G(r.containerSelectors,".Container",null,o),v=G(r.headerSelectors,".Header",null,o),k=G(r.footerSelectors,".Footer",null,o),S=G(r.footerLink,".Footer--link",null,o),x={colorBackground:m,colorText:c.color,fontFamily:c.fontFamily,fontSizeBase:c.fontSize};r.pmmeRelativeTextSizeSelector&&x.fontSizeBase&&(x.fontSizeBase=function(e,t,o=.875,r){const n=parseFloat(t);if(isNaN(n))return t;const a=r.querySelector(e);if(!a)return n*o+"px";const i=window.getComputedStyle(a).getPropertyValue("font-size"),s=parseFloat(i)*o;return isNaN(s)?t:n>s?`${s}px`:`${n}px`}(r.pmmeRelativeTextSizeSelector,c.fontSize,.875,o));const C="blocks_checkout"===e;let T={variables:x,theme:(I=m,f(I).getBrightness()>125?"stripe":"night"),labels:C?"floating":"above",rules:JSON.parse(JSON.stringify({".Input":n,".Input--invalid":a,".Label":i,".Label--resting":s,".Block":g,".Tab":l,".Tab:hover":d,".Tab--selected":u,".TabIcon:hover":p,".TabIcon--selected":h,".Text":c,".Text--redirect":c}))};var I;return C&&(T=((e,t)=>{if(e.rules[".Label--floating"]=t,e.rules[".Label--floating"].transform&&"none"!==e.rules[".Label--floating"].transform){const t=e.rules[".Label--floating"].transform.match(/matrix\((.+)\)/);if(t&&t[1]){const o=t[1].split(", "),r=(parseFloat(o[0])+parseFloat(o[3]))/2,n=parseFloat(e.rules[".Label--floating"].lineHeight),a=Math.floor(n*r);e.rules[".Label--floating"].lineHeight=`${a}px`,e.rules[".Label--floating"].fontSize=`${a}px`}delete e.rules[".Label--floating"].transform}if(e.rules[".Input"].paddingTop&&(e.rules[".Input"].paddingTop=`calc(${e.rules[".Input"].paddingTop} - ${e.rules[".Label--floating"].lineHeight} - 4px - 1px)`),e.rules[".Input"].paddingBottom){var o;const t=parseFloat(e.rules[".Input"].paddingBottom);e.rules[".Input"].paddingBottom=t-1+"px";const r=null!==(o=e.rules[".Label"].marginTop)&&void 0!==o?o:"0";e.rules[".Label"].marginTop=`${Math.floor((t-1)/3)}px`,e.rules[".Label--floating"].marginTop=r}return e})(T,G(r.hiddenValidActiveLabel,".Label--floating",null,o))),t&&(T.rules={...T.rules,".Heading":b,".Header":v,".Footer":k,".Footer-link":S,".Button":y,".Link":_,".Container":w}),X.cleanup(o),T};class K{constructor(e,t){this.options=e,this.stripe=null,this.stripePlatform=null,this.request=t,this.isWooPayRequesting=!1}createStripe(e,t,o="",r=[]){const n={locale:t};return o&&(n.stripeAccount=o),r&&(n.betas=r),new Stripe(e,n)}async getStripeForUPE(e){return this.options.forceNetworkSavedCards=n("paymentMethodsConfig")[e].forceNetworkSavedCards,this.getStripe()}async getStripe(e=!1){let t=0;for(;!window.Stripe;)if(await new Promise((e=>setTimeout(e,100))),t+=100,t>6e5)throw new Error("Stripe object not found");return this.__getStripe(e)}__getStripe(e=!1){const{publishableKey:t,accountId:o,forceNetworkSavedCards:r,locale:n,isStripeLinkEnabled:a}=this.options;if(r&&!e)return this.stripePlatform||(this.stripePlatform=this.createStripe(t,n)),this.stripePlatform;if(!this.stripe){let e=["card_country_event_beta_1"];a&&(e=e.concat(["link_autofill_modal_beta_1"])),this.stripe=this.createStripe(t,n,o,e)}return this.stripe}async loadStripeForExpressCheckout(){try{return this.getStripe(!0)}catch(e){return{error:e}}}confirmIntent(e,t=!1){const o=e.match(/#wcpay-confirm-(pi|si):(.+):(.+):(.+)$/);if(!o)return!0;const n="si"===o[1];let i=o[2];const s=o[3],c=o[4],l=e.indexOf("order-pay"),u=l>-1&&e.substring(l).match(/\d+/);return u&&(i=u[0]),(async()=>{const{locale:e,publishableKey:t}=this.options,o=r("accountIdForIntentConfirmation"),a=await this.getStripe();return n?a.handleNextAction({clientSecret:s}):o?this.createStripe(t,e,o).confirmCardPayment(s):(await this.getStripe(!0)).handleNextAction({clientSecret:s})})().then((e=>{var o;let n=null;e.paymentIntent?.last_payment_error&&(n={message:e.paymentIntent.last_payment_error.message}),"requires_action"===e.paymentIntent?.status&&(n={message:"Payment requires additional action."});const s=e.paymentIntent&&e.paymentIntent.id||e.setupIntent&&e.setupIntent.id||e.error&&e.error.payment_intent&&e.error.payment_intent.id||e.error.setup_intent&&e.error.setup_intent.id,l=null!==(o=a("ajax_url"))&&void 0!==o?o:r("ajaxUrl"),u=r("isChangingPayment");return[this.request(l,{action:"update_order_status",order_id:i,_ajax_nonce:c,intent_id:s,should_save_payment_method:t?"true":"false",is_changing_payment:u?"true":"false"}),n,e.error]})).then((([e,t,o])=>{if(o)throw o;return e.then((e=>{const o="string"==typeof e?JSON.parse(e):e;if(o.error)throw o.error;if(t)throw t;return o.return_url}))}))}async setupIntent(e){const t=await this.request(r("ajaxUrl"),{action:"create_setup_intent","wcpay-payment-method":e,_ajax_nonce:r("createSetupIntentNonce")});if(!t.success)throw t.data.error;if("succeeded"===t.data.status)return t.data;const o=await this.getStripe(),n=await o.confirmCardSetup(t.data.client_secret),{setupIntent:a,error:i}=n;if(i)throw i;return a}saveUPEAppearance(e,t){return this.request(r("ajaxUrl"),{elements_location:t,appearance:JSON.stringify(e),action:"save_upe_appearance",_ajax_nonce:r("saveUPEAppearanceNonce")}).then((e=>e.data)).catch((e=>{throw e.message?e:new Error(e.statusText)}))}initWooPay(e,t){if(!this.isWooPayRequesting){this.isWooPayRequesting=!0;const o=r("wcAjaxUrl"),n=r("initWooPayNonce"),a=document.querySelector(".wp-block-woocommerce-checkout")?"blocks_checkout":document.querySelector(".woocommerce-billing-fields")?"woopay_shortcode_checkout":document.querySelector(".wp-block-woocommerce-cart")?"bnpl_cart_block":document.querySelector(".woocommerce-cart-form")?"bnpl_classic_cart":document.querySelector(".single-product")?"bnpl_product_page":void 0;return this.request(i(o,"init_woopay"),{_wpnonce:n,appearance:r("isWooPayGlobalThemeSupportEnabled")?J(a,!0):null,email:e,user_session:t,order_id:r("order_id"),key:r("key"),billing_email:r("billing_email")}).finally((()=>{this.isWooPayRequesting=!1}))}}expressCheckoutAddToCart(e){const t=r("wcAjaxUrl"),o=r("addToCartNonce");return this.request(i(t,"add_to_cart"),{security:o,...e})}}jQuery((e=>{e("input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]").on("change",(()=>{(0,o.doAction)("wcpay.express-checkout.update-button-data")}))})),(0,o.addFilter)("wcpay.express-checkout.cart-add-item","automattic/wcpay/express-checkout",(e=>{const t={};return jQuery("input[name=wc_deposit_option]").length&&(t.wc_deposit_option=jQuery("input[name=wc_deposit_option]:checked").val()),jQuery("input[name=wc_deposit_payment_plan]").length&&(t.wc_deposit_payment_plan=jQuery("input[name=wc_deposit_payment_plan]:checked").val()),{...e,...t}}));const Z="wcpay-express-checkout__order-attribution-inputs",Y=()=>{(()=>{if(document.getElementById(Z))return;const e=document.createElement("wc-order-attribution-inputs");e.id=Z,document.body.appendChild(e)})(),window?.wc_order_attribution?.setOrderTracking(window?.wc_order_attribution?.params?.allowTracking)};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",Y):Y(),(0,o.addFilter)("wcpay.express-checkout.cart-place-order-extension-data","automattic/wcpay/express-checkout",(e=>{const t=jQuery("#wcpay-express-checkout__order-attribution-inputs input");if(!t.length)return e;const o={};return t.each((function(){const e=jQuery(this).attr("name").replace("wc_order_attribution_",""),t=jQuery(this).val();e&&t&&(o[e]=t)})),{...e,"woocommerce/order-attribution":o}}));let ee=null,te=null;const oe=()=>jQuery(te),re={init:({elementId:e,$separator:t})=>{te=e,ee=t},getButtonSeparator:()=>ee,blockButton:()=>{oe().data("blockUI.isBlocked")||oe().block({message:null})},unblockButton:()=>{re.showContainer(),oe().unblock()},renderButton:e=>{oe()?.length&&e.mount(te)},hideContainer:()=>{oe().removeClass("is-ready").hide()},showContainer:()=>{oe().addClass("is-ready").show()}},ne=re,ae=(e,t,o=!1)=>{let r;return function(){const n=this,a=arguments,i=o&&!r;clearTimeout(r),r=setTimeout((()=>{r=null,o||t.apply(n,a)}),e),i&&t.apply(n,a)}},ie=e=>{var t,o,r,n;return void 0!==window.wcpayExpressCheckoutParams?null!==(t=window.wcpayExpressCheckoutParams[e])&&void 0!==t?t:null:void 0!==(null===(o=window.wc)||void 0===o?void 0:o.wcSettings)&&null!==(n=null===(r=window.wc.wcSettings.getSetting("ece_data"))||void 0===r?void 0:r[e])&&void 0!==n?n:null},se=e=>{if(!e)return"";const t=document.createElement("div");return t.innerHTML=e.trim(),t.firstChild?t.firstChild.textContent:""},ce=e=>{var t,o;let r="undefined"==typeof wcpaySettings?4:parseInt(window?.wcpaySettings?.defaultExpressCheckoutBorderRadius||4,10);const n=ie("button");return r=null!==(t=null==n?void 0:n.radius)&&void 0!==t?t:r,void 0!==e&&(r=null!==(o=Number(null==e?void 0:e.borderRadius))&&void 0!==o?o:r),{variables:{borderRadius:`${r}px`,spacingUnit:"6px"}}},le=()=>{var e,t;const o=null!==(t=null===(e=ie("product"))||void 0===e?void 0:e.product_type)&&void 0!==t?t:"",r=["subscription","variable-subscription","subscription_variation"].includes(o);return ie("has_subscription")||r?{setupFutureUsage:"off_session"}:{}};jQuery((e=>{e(document.body).on("woocommerce_variation_has_changed",(async()=>{(0,o.doAction)("wcpay.express-checkout.update-button-data")}))})),jQuery((e=>{if("product"!==ie("button_context"))return;const t=e(".quantity");t.on("input",".qty",(()=>{ne.blockButton()})),t.on("input",".qty",ae(250,(async()=>{(0,o.doAction)("wcpay.express-checkout.update-button-data")})))})),(0,o.addFilter)("wcpay.express-checkout.cart-add-item","automattic/wcpay/express-checkout",(e=>{const t=document.querySelector(".single_variation_wrap");if(!t)return e;const o=t.querySelector('input[name="product_id"]').value;return{...e,id:parseInt(o,10)}})),(0,o.addFilter)("wcpay.express-checkout.cart-add-item","automattic/wcpay/express-checkout",(e=>{const t=document.querySelector(".variations_form");if(!t)return e;const o=[],r=t.querySelectorAll(".variations select");return Array.from(r).forEach((function(e){const t=e.dataset.attribute_name||e.dataset.name;o.push({attribute:Array.from(document.querySelector(`label[for="${t.replace("attribute_","")}"]`).childNodes)[0].textContent,value:e.value||""}),o.push({attribute:t,value:e.value||""})})),{...e,variation:[...e.variation,...o]}})),(0,o.addFilter)("wcpay.express-checkout.map-line-items","automattic/wcpay/express-checkout",(e=>({...e,items:e.items.filter((e=>!e.extensions?.bundles?.bundled_by))})));const ue=(e,t,o)=>{const r=document.querySelector(`${e} [name="${t}"]`);if(r)if("SELECT"===r.tagName&&/country|state/.test(t)){const e=Array.from(r.options).find((e=>e.value===o||e.textContent.trim().toLowerCase()===o.toLowerCase()));e&&(r.value=e.value,jQuery(r).trigger("change").trigger("close"))}else r.value=o,jQuery(r).trigger("change")},de=window.lodash;window.wp.domReady;const pe=(e,t={})=>{var o,n,i,s;if(!1===(null!==(o=r("isShopperTrackingEnabled"))&&void 0!==o?o:a("is_shopper_tracking_enabled")))return;const c=null!==(n=r("platformTrackerNonce"))&&void 0!==n?n:null===(i=a("nonce"))||void 0===i?void 0:i.platform_tracker,l=null!==(s=r("ajaxUrl"))&&void 0!==s?s:a("ajax_url"),u=new FormData;u.append("tracksNonce",c),u.append("action","platform_tracks"),u.append("tracksEventName",e),u.append("tracksEventProp",JSON.stringify(t)),fetch(l,{method:"post",body:u}).then((e=>e.json()))},he=(0,de.debounce)((({source:e})=>{pe("applepay_button_load",{source:e})}),1e3),me=(0,de.debounce)((({source:e})=>{pe("gpay_button_load",{source:e})}),1e3),fe=window.wp.apiFetch;var be=e.n(fe);const ge=window.wp.url;class ye{cartRequestHeaders={};async _request(e){const t=await be()({...e,parse:!1,path:(0,ge.addQueryArgs)(e.path,{currency:ie("checkout").currency_code.toUpperCase()}),headers:{Nonce:ie("nonce").store_api_nonce,"X-WooPayments-Tokenized-Cart-Nonce":ie("nonce").tokenized_cart_nonce||void 0,"X-WooPayments-Tokenized-Cart-Session-Nonce":"product"===ie("button_context")?ie("nonce").tokenized_cart_session_nonce:void 0,...this.cartRequestHeaders,...e.headers}});return this.cartRequestHeaders={Nonce:t.headers.get("Nonce"),"X-WooPayments-Tokenized-Cart-Session":t.headers.get("X-WooPayments-Tokenized-Cart-Session")},t.json()}async placeOrder(e){return await this._request({method:"POST",path:"/wc/store/v1/checkout",headers:{"X-WooPayments-Tokenized-Cart":!0,...this.cartRequestHeaders},data:e})}async getCart(){return await this._request({method:"GET",path:"/wc/store/v1/cart"})}useSeparateCart(){this.cartRequestHeaders={"X-WooPayments-Tokenized-Cart-Session":""}}deleteAfterRequest(){this.cartRequestHeaders={"X-WooPayments-Tokenized-Cart-Is-Ephemeral-Cart":"1"}}async updateCustomer(e){return await this._request({method:"POST",path:"/wc/store/v1/cart/update-customer",headers:{"X-WooPayments-Tokenized-Cart":!0,...this.cartRequestHeaders},data:e})}async selectShippingRate(e){return await this._request({method:"POST",path:"/wc/store/v1/cart/select-shipping-rate",data:e})}async addProductToCart(){const e={id:jQuery(".single_add_to_cart_button").val(),quantity:parseInt(jQuery(".quantity .qty").val(),10)||1,variation:[]};return await this._request({method:"POST",path:"/wc/store/v1/cart/add-item",data:(0,o.applyFilters)("wcpay.express-checkout.cart-add-item",e)})}async emptyCart(){try{let e=await this._request({method:"GET",path:"/wc/store/v1/cart"});for(;e.items.length>0;)e=await this._request({method:"POST",path:"/wc/store/v1/cart/remove-item",data:{key:e.items[0].key}})}catch(e){}}}const _e=(e,t)=>{var o,r,n,a,i,s,c,l,u;return{first_name:null!==(o=e?.split(" ")?.slice(0,1)?.join(" "))&&void 0!==o?o:"",last_name:null!==(r=e?.split(" ")?.slice(1)?.join(" "))&&void 0!==r?r:"",company:null!==(n=t.organization)&&void 0!==n?n:"",address_1:null!==(a=t.line1)&&void 0!==a?a:"",address_2:null!==(i=t.line2)&&void 0!==i?i:"",city:null!==(s=t.city)&&void 0!==s?s:"",state:null!==(c=t.state)&&void 0!==c?c:"",postcode:null!==(l=t.postal_code?.replace(" ",""))&&void 0!==l?l:"",country:null!==(u=t.country)&&void 0!==u?u:""}},we=(e,t,o=!0,r=[])=>{var n,a,i,s,c,l,u,d,p,h,m,f,b;const g=e.billingDetails?.name||"",y=null!==(n=e.billingDetails?.address)&&void 0!==n?n:{},_=null!==(a=null!==(i=e.billingDetails?.phone?.replace(/[() -]/g,""))&&void 0!==i?i:e.payerPhone?.replace(/[() -]/g,""))&&void 0!==a?a:"";return{customer_note:e.order_comments,billing_address:{first_name:null!==(s=g.split(" ")?.slice(0,1)?.join(" "))&&void 0!==s?s:"",last_name:g.split(" ")?.slice(1)?.join(" ")||"-",company:null!==(c=y.organization)&&void 0!==c?c:"",address_1:null!==(l=y.line1)&&void 0!==l?l:"",address_2:null!==(u=y.line2)&&void 0!==u?u:"",city:null!==(d=y.city)&&void 0!==d?d:"",state:null!==(p=y.state)&&void 0!==p?p:"",postcode:null!==(h=y.postal_code)&&void 0!==h?h:"",country:null!==(m=y.country)&&void 0!==m?m:"",email:null!==(f=e.billingDetails?.email)&&void 0!==f?f:"",phone:_},shipping_address:e.shippingAddress?{..._e(e.shippingAddress.name||"",e.shippingAddress.address),phone:_}:void 0,payment_method:"woocommerce_payments",payment_data:[{key:"payment_method",value:"card"},{key:"wcpay-fraud-prevention-token",value:null!==(b=window.wcpayFraudPreventionToken)&&void 0!==b?b:""},{key:o?"wcpay-confirmation-token":"wcpay-payment-method",value:t},{key:"express_payment_type",value:e.expressPaymentType},{key:"wcpay-express-payment-method-types",value:JSON.stringify(r)}]}},ve=window.wp.htmlEntities,ke=(e,t)=>{var o;return e*10**((null!==(o=ie("checkout")?.currency_decimals)&&void 0!==o?o:2)-t.currency_minor_unit)},Se=e=>{const r=ie("checkout").display_prices_with_tax,n=(0,o.applyFilters)("wcpay.express-checkout.map-line-items",e),a=n.items.map((e=>({amount:ke(r&&e.totals?parseInt(e.totals.line_subtotal,10)+parseInt(e.totals.line_subtotal_tax,10):parseInt(e.totals?.line_subtotal||e.prices.price,10),e.totals||e.prices),name:[e.name,e.quantity>1&&`(x${e.quantity})`,e.variation&&e.variation.length>0&&"-",e.variation&&e.variation.map((e=>`${e.attribute}: ${e.value}`)).join(", "),e.item_data&&e.item_data.length>0&&"-",e.item_data&&e.item_data.map((e=>`${e.name||e.key}: ${e.value}`)).join(", ")].filter(Boolean).map(ve.decodeEntities).join(" ")}))),i=parseInt(n.totals.total_shipping||"0",10);i&&a.push({amount:ke(r?i+parseInt(n.totals.total_shipping_tax||"0",10):i,n.totals),name:(0,t.__)("Shipping","woocommerce-payments")});const s=parseInt(n.totals.total_discount||"0",10);s&&a.push({amount:-ke(r?s+parseInt(n.totals.total_discount_tax||"0",10):s,n.totals),name:(0,t.__)("Discount","woocommerce-payments")});const c=parseInt(n.totals.total_fees||"0",10);c&&a.push({amount:ke(r?c+parseInt(n.totals.total_fees_tax||"0",10):c,n.totals),name:(0,t.__)("Fees","woocommerce-payments")});const l=parseInt(n.totals.total_tax||"0",10);l&&!r&&a.push({amount:ke(l,n.totals),name:(0,t.__)("Tax","woocommerce-payments")});const u=parseInt(n.totals.total_refund||"0",10);return u&&a.push({amount:-ke(u,n.totals),name:(0,t.__)("Refund","woocommerce-payments")}),ke(parseInt(n.totals.total_price,10)-parseInt(n.totals.total_refund||0,10),n.totals)<a.reduce(((e,{amount:t})=>e+t),0)?[]:a},xe=e=>{const t=ie("checkout").display_prices_with_tax;return e.shipping_rates?.[0]?.shipping_rates.sort(((e,t)=>e.selected===t.selected?0:e.selected?-1:1)).slice(0,9).map((e=>({id:e.rate_id,displayName:(0,ve.decodeEntities)(e.name),amount:ke(t?parseInt(e.price,10)+parseInt(e.taxes,10):parseInt(e.price,10),e),deliveryEstimate:[e.meta_data.find((e=>"pickup_address"===e.key))?.value,e.meta_data.find((e=>"pickup_details"===e.key))?.value].filter(Boolean).map(ve.decodeEntities).join(" - ")})))};let Ce=null,Te=new ye;const Ie=()=>Te,Ae=()=>{jQuery.blockUI({message:null,overlayCSS:{background:"#fff",opacity:.6}})},Pe=()=>{jQuery.unblockUI()};class Me{orderId;key;billingEmail="";cachedCartData={};constructor({orderId:e,key:t,billingEmail:o=""}){this.orderId=e,this.key=t,this.billingEmail=o}async placeOrder(e){return await be()({method:"POST",path:`/wc/store/v1/checkout/${this.orderId}`,headers:{Nonce:ie("nonce").store_api_nonce},data:{...e,key:this.key,billing_email:this.billingEmail,billing_address:this.cachedCartData.billing_address,shipping_address:this.cachedCartData.shipping_address}})}async getCart(){return this.cachedCartData=await be()({method:"GET",path:(0,ge.addQueryArgs)(`/wc/store/v1/order/${this.orderId}`,{key:this.key,billing_email:this.billingEmail})})}}let qe=null;const Re=async()=>{if("product"!==ie("button_context"))return await Ie().getCart();const e=new ye;return e.useSeparateCart(),e.deleteAfterRequest(),await e.addProductToCart()},Ee=()=>qe?ke(parseInt(qe.totals.total_price,10)-parseInt(qe.totals.total_refund||0,10),qe.totals):"product"===ie("button_context")&&ie("product")?ie("product")?.total.amount:void 0;let Le;jQuery((e=>{if(ie("has_block")&&"pay_for_order"!==ie("button_context"))return;const n=ie("stripe").publishableKey;if(!n)return;const a=new K({publishableKey:n,accountId:ie("stripe").accountId,locale:ie("stripe").locale},((e,t)=>new Promise(((o,r)=>{jQuery.post(e,t).then(o).fail(r)}))));var i;"pay_for_order"===ie("button_context")&&(i=new Me({orderId:r("order_id"),key:r("key"),billingEmail:r("billing_email")}),Te=i),ne.init({elementId:"#wcpay-express-checkout-element",$separator:jQuery("#wcpay-express-checkout-button-separator")});const s={abortPayment:t=>{Pe(),e(".woocommerce-error").remove();const o=e(".woocommerce-notices-wrapper").first();o.length&&(o.append(e('<div class="woocommerce-error" />').text(t)),e("html, body").animate({scrollTop:o.find(".woocommerce-error").offset().top},600))},completePayment:e=>{Ae(),window.location=e},startExpressCheckoutElement:async r=>{var n,i,c;let l="",u=Promise.resolve();const d=await a.getStripe(),p=null===(n=ie("flags")?.isEceUsingConfirmationTokens)||void 0===n||n,h=null!==(i=ie("enabled_methods"))&&void 0!==i?i:[],m=[h.includes("payment_request")&&"card",h.includes("amazon_pay")&&"amazon_pay"].filter(Boolean);Le=d.elements({mode:"payment",amount:r.total,currency:r.currency,...p?{paymentMethodTypes:m,...le()}:{paymentMethodCreation:"manual"},appearance:ce(),locale:null!==(c=ie("stripe")?.locale)&&void 0!==c?c:"en"});const f=Le.create("expressCheckout",(()=>{var e,t,o,r,n,a;const i=ie("button"),s=null!==(e=ie("enabled_methods"))&&void 0!==e?e:[],c=(e,t)=>{switch(t){case"dark":default:return"black";case"light":return"white";case"light-outline":return"googlePay"===e?"white":"white-outline"}},l="default"===(null==i?void 0:i.type)?"plain":null!==(t=null==i?void 0:i.type)&&void 0!==t?t:"buy",u="default"===(null==i?void 0:i.type)?"plain":null!==(o=null==i?void 0:i.type)&&void 0!==o?o:"plain",d=s.includes("payment_request");return{paymentMethods:{applePay:d?"always":"never",googlePay:d?"always":"never",amazonPay:s.includes("amazon_pay")?"auto":"never",link:"never",paypal:"never",klarna:"never"},layout:{overflow:"never"},buttonTheme:{googlePay:c("googlePay",null!==(r=null==i?void 0:i.theme)&&void 0!==r?r:"black"),applePay:c("applePay",null!==(n=null==i?void 0:i.theme)&&void 0!==n?n:"black")},buttonType:{googlePay:l,applePay:u},buttonHeight:Math.min(Math.max(parseInt(null!==(a=null==i?void 0:i.height)&&void 0!==a?a:"48",10),40),55)}})());ne.renderButton(f),f.on("loaderror",(()=>{document.getElementById("wcpay-woopay-button")||ne.getButtonSeparator().hide()})),f.on("click",(function(o){if(l="",ie("login_confirmation"))return void(e=>{const t=ie("login_confirmation");if(!t)return;let o=t.message;o=o.replace(/\*\*.*?\*\*/,{apple_pay:"Apple Pay",google_pay:"Google Pay",amazon_pay:"Amazon Pay",paypal:"PayPal",link:"Link"}[e]),o=o.replace(/\*\*/g,""),confirm(o)&&(window.location.href=t.redirect_url)})(o.expressPaymentType);if("product"===ie("button_context")){const o=e(".single_add_to_cart_button");if(o.is(".disabled"))return void(o.is(".wc-variation-is-unavailable")?window.alert(window?.wc_add_to_cart_variation_params?.i18n_unavailable_text||(0,t.__)("Sorry, this product is unavailable. Please choose a different combination.","woocommerce-payments")):window.alert((0,t.__)("Please select your product options before proceeding.","woocommerce-payments")));Ie().useSeparateCart(),u=Ie().addProductToCart(),u.catch((()=>{l=(0,t.__)("There was an error processing the product with this payment method. Please add the product to the cart, instead.","woocommerce-payments"),Ie().emptyCart(),f.unmount(),Le=null,s.abortPayment(l),ne.hideContainer(),ne.getButtonSeparator().hide()})).finally((()=>{u=Promise.resolve()}))}const r=qe?{shippingAddressRequired:"pay_for_order"!==ie("button_context")&&qe.needs_shipping,shippingRates:xe(qe),phoneNumberRequired:null!==(n=ie("checkout")?.needs_payer_phone)&&void 0!==n&&n,lineItems:Se(qe)}:"product"===ie("button_context")&&ie("product")?{shippingAddressRequired:null!==(a=ie("product")?.needs_shipping)&&void 0!==a&&a,phoneNumberRequired:null!==(i=ie("checkout")?.needs_payer_phone)&&void 0!==i&&i,lineItems:(null!==(c=ie("product")?.displayItems)&&void 0!==c?c:[]).map((({label:e,amount:t})=>({name:e,amount:t})))}:void 0;var n,a,i,c;const d=r.shippingRates&&0!==r.shippingRates.length?r.shippingRates:[{id:"pending",displayName:(0,t.__)("Pending","woocommerce-payments"),amount:0}];(async function({expressPaymentType:e}){Ae(),((e,t)=>{const o={google_pay:"gpay_button_click",apple_pay:"applepay_button_click"}[e];o&&pe(o,{source:t})})(e,ie("button_context"))})(o),o.resolve({business:{name:ie("store_name")},emailRequired:!0,...r,shippingRates:r.shippingAddressRequired?d:void 0,allowedShippingCountries:ie("checkout").allowed_shipping_countries})})),f.on("shippingaddresschange",(async e=>(await u,l?e.resolve():(async(e,t)=>{Ce=e.address;try{const o=await Te.updateCustomer({shipping_address:_e(e.name,e.address)}),r=xe(o);if(0===r.length)return void e.reject();t.update({amount:ke(parseInt(o.totals.total_price,10)-parseInt(o.totals.total_refund||0,10),o.totals)}),e.resolve({shippingRates:r,lineItems:Se(o)})}catch(t){e.reject()}})(e,Le)))),f.on("shippingratechange",(async e=>(async(e,t)=>{try{const o=await Te.selectShippingRate({package_id:0,rate_id:e.shippingRate.id});t.update({amount:ke(parseInt(o.totals.total_price,10)-parseInt(o.totals.total_refund||0,10),o.totals)}),e.resolve({lineItems:Se(o)})}catch(t){e.reject()}})(e,Le))),f.on("confirm",(async e=>{if(!l)return(async(e,r,n,a,i,s,c=[])=>{var l;const{error:u}=await n.submit();if(u)return i(u.message);const d=null===(l=ie("flags")?.isEceUsingConfirmationTokens)||void 0===l||l;let p;if(d){const{confirmationToken:e,error:t}=await r.createConfirmationToken({elements:n});if(t)return i(t.message);p=e}else{const{paymentMethod:e,error:t}=await r.createPaymentMethod({elements:n});if(t)return i(t.message);p=e}try{const t=await Te.placeOrder({...we(s,p.id,d,c),extensions:(0,o.applyFilters)("wcpay.express-checkout.cart-place-order-extension-data",{})});var h,m;if("success"!==t.payment_result.payment_status)return i(se(null!==(h=null!==(m=t.message)&&void 0!==m?m:t.payment_result?.payment_details.find((e=>"errorMessage"===e.key))?.value)&&void 0!==h?h:""));let r=t.payment_result.redirect_url;if(!r){const e=t.payment_result.payment_details?.find((e=>"redirect"===e.key));r=e?.value||""}const n=e.confirmIntent(r);a(!0===n?r:await n)}catch(e){return e.json&&(e=await Promise.resolve(e.json())),i(se(e.message||e.payment_result?.payment_details.find((e=>"errorMessage"===e.key))?.value||(0,t.__)("There was a problem processing the order.","woocommerce-payments")))}})(a,d,Le,s.completePayment,s.abortPayment,e,m);s.abortPayment(l)})),f.on("cancel",(()=>{"product"===ie("button_context")&&u.finally((()=>{Ie().emptyCart()})),Ce&&(e=>{const t=ie("button_context"),o=ie("has_block");var r;["cart","checkout"].includes(t)&&(o?wc?.blocksCheckout?.extensionCartUpdate({namespace:"woopayments/express-checkout/refresh-ui",data:{}}):(r=e.country,["CA","GB"].includes(r)||(e=>{const t=ie("button_context"),o=(r=e,{first_name:null!==(n=r?.recipient?.split(" ")?.slice(0,1)?.join(" "))&&void 0!==n?n:"",last_name:null!==(a=r?.recipient?.split(" ")?.slice(1)?.join(" "))&&void 0!==a?a:"",company:"",address_1:null!==(i=r?.addressLine?.[0])&&void 0!==i?i:"",address_2:null!==(s=r?.addressLine?.[1])&&void 0!==s?s:"",city:null!==(c=r?.city)&&void 0!==c?c:"",state:null!==(l=r?.state)&&void 0!==l?l:"",country:null!==(u=r?.country)&&void 0!==u?u:"",postcode:null!==(d=r?.postal_code)&&void 0!==d?d:""});var r,n,a,i,s,c,l,u,d;const p=["country","state","city","postcode"];"cart"===t?(p.forEach((e=>{o[e]&&ue("form.woocommerce-shipping-calculator",`calc_shipping_${e}`,o[e])})),document.querySelector('form.woocommerce-shipping-calculator [name="calc_shipping"]')?.click()):"checkout"===t&&p.forEach((e=>{o[e]&&ue("form.woocommerce-checkout",`billing_${e}`,o[e])}))})(e)))})(Ce),Ce=null,Pe()})),f.on("ready",(e=>{(async function({availablePaymentMethods:e}){e&&(({paymentMethods:e,source:t})=>{const o={googlePay:me,applePay:he};for(const r of e){const e=o[r];e&&e({source:t})}})({paymentMethods:Object.entries(e).filter((([e,t])=>t)).map((([e,t])=>e)),source:ie("button_context")})})(e),e?.availablePaymentMethods&&Object.values(e.availablePaymentMethods).filter(Boolean).length&&(ne.showContainer(),ne.getButtonSeparator().show())}))},init:async()=>{if((0,o.removeAction)("wcpay.express-checkout.update-button-data","automattic/wcpay/express-checkout"),"product"===ie("button_context")&&"bundle"===ie("product")?.product_type&&(wcpayExpressCheckoutParams.product=void 0),!ie("product")&&!qe)try{qe=await Re()}catch(e){}qe&&(wcpayExpressCheckoutParams.product=void 0);const t=Ee();0===t?(ne.hideContainer(),ne.getButtonSeparator().hide()):qe?await s.startExpressCheckoutElement({total:t,currency:qe.totals.currency_code.toLowerCase()}):"product"===ie("button_context")&&ie("product")?await s.startExpressCheckoutElement({total:t,currency:ie("product")?.currency}):(ne.hideContainer(),ne.getButtonSeparator().hide()),(0,o.addAction)("wcpay.express-checkout.update-button-data","automattic/wcpay/express-checkout",(async()=>{if("product"===ie("button_context")&&e(".single_add_to_cart_button").is(".disabled"))ne.unblockButton();else{e(".woocommerce-error").remove();try{ne.blockButton();const e=Ee();qe=await Re(),wcpayExpressCheckoutParams.product=null,ne.unblockButton();const t=Ee();Le?t!==e&&t>0&&Le.update({amount:t}):s.init(),0===t?(ne.hideContainer(),ne.getButtonSeparator().hide()):(ne.showContainer(),ne.getButtonSeparator().show())}catch(e){ne.hideContainer()}}}))}};"checkout"===ie("button_context")&&"pay_for_order"!==ie("button_context")||s.init(),e(document.body).on("updated_cart_totals",(()=>{qe=null,s.init()})),e(document.body).on("updated_checkout",(()=>{qe=null,s.init()}))}))})()})(); dist/success.asset.php 0000777 00000000141 15251156605 0011017 0 ustar 00 <?php return array('dependencies' => array('wp-polyfill'), 'version' => '48af1fb535b27c54a464'); dist/multi-currency.css 0000777 00000053502 15251156605 0011225 0 ustar 00 .task-collapsible-body{max-height:0;overflow:hidden;opacity:0;transition:max-height 500ms ease-out,opacity 500ms ease-out}.task-collapsible-body.is-active{overflow:visible;max-height:1000px;opacity:1;transition:max-height 500ms ease-in,opacity 500ms ease-in} .wcpay-wizard-task{margin-bottom:0}.wcpay-wizard-task:first-child .wcpay-wizard-task__top-border{display:none}.wcpay-wizard-task:last-child .wcpay-wizard-task__body{border-left:none}.wcpay-wizard-task__top-border,.wcpay-wizard-task__body{min-height:16px;border-left:1px solid #e0e0e0;margin:0 11px}.wcpay-wizard-task__headline{display:flex;align-items:center;flex-direction:row;margin:4px 0;font-weight:600;font-size:16px;color:#2c3338}.is-active .wcpay-wizard-task__headline{color:var(--wp-admin-theme-color)}.wcpay-wizard-task__title{padding:0 16px;display:flex;align-items:center;flex-wrap:wrap}.wcpay-wizard-task__title>*:not(:last-child){margin-right:4px}.wcpay-wizard-task__icon-wrapper{position:relative;width:24px;height:24px;padding:0;border-radius:9999px;border:1px solid #e0e0e0;display:flex;justify-content:center;align-items:center;flex-shrink:0}.is-active .wcpay-wizard-task__icon-wrapper{border-color:var(--wp-admin-theme-color)}.is-completed .wcpay-wizard-task__icon-wrapper{border-color:var(--wp-admin-theme-color);background-color:var(--wp-admin-theme-color)}.wcpay-wizard-task__icon-text{font-size:13px}.is-completed .wcpay-wizard-task__icon-text{opacity:0}.wcpay-wizard-task__icon-checkmark{opacity:0;transition:opacity 500ms ease;position:absolute;top:0;right:0;width:100%;height:100%;fill:#fff}.is-completed .wcpay-wizard-task__icon-checkmark{opacity:1}.wcpay-wizard-task__body{display:flex;flex-direction:column;padding:0 30px;font-size:13px}.wcpay-wizard-task__description-element{margin-top:0;margin-bottom:1em}.wcpay-wizard-task__description-element.is-muted-color{color:#757575}.wcpay-wizard-task__description-element .components-external-link svg{width:1em;height:1em}.wcpay-wizard-task__visible-description-element{position:absolute;margin-left:40px;margin-top:0;margin-bottom:1em}.wcpay-wizard-task__visible-description-element.is-muted-color{color:#757575}.wcpay-wizard-task__visible-description-element .components-external-link svg{width:1em;height:1em}.wcpay-wizard-task .add-payment-methods-task__payment-selector-wrapper{margin-bottom:24px}.wcpay-wizard-task .add-payment-methods-task__payment-selector-title{font-weight:600;font-size:16px}.wcpay-wizard-task .add-payment-methods-task__payment-selector-description{margin-bottom:24px} .wcpay-confirmation-modal.wcpay-confirmation-modal{max-width:600px}.wcpay-confirmation-modal .components-modal__content{display:flex;flex-direction:column;padding:0 24px 24px}.wcpay-confirmation-modal .components-modal__content p{padding:0 0 1em 0;margin:0}.wcpay-confirmation-modal .components-modal__content p.no-padding{padding:0}.wcpay-confirmation-modal .components-modal__header{padding:24px}.wcpay-confirmation-modal__separator{margin:24px -24px}.wcpay-confirmation-modal__footer{display:flex;justify-content:flex-end}.wcpay-confirmation-modal__footer>*:not(:first-child){margin-left:16px} .wcpay-inline-notice.components-notice{margin:24px 0;padding:11px 0 11px 17px;border-left:none;border-radius:2px;justify-content:flex-start}.components-modal__header+.wcpay-inline-notice.components-notice,.wcpay-inline-notice.components-notice:first-child{margin-top:0}.wcpay-inline-notice.components-notice:last-child{margin-bottom:0}.wcpay-inline-notice.components-notice .wcpay-inline-notice__icon{display:flex;align-items:center;align-self:flex-start;min-height:auto;min-width:auto;margin-right:5px}.wcpay-inline-notice.components-notice .wcpay-inline-notice__icon svg{width:22px;height:22px}.wcpay-inline-notice.components-notice .components-notice__content{margin-top:2px;margin-bottom:2px}.wcpay-inline-notice.components-notice .components-notice__content a{white-space:nowrap}.wcpay-inline-notice.components-notice .wcpay-inline-notice__content__actions{padding-top:12px}.wcpay-inline-notice.components-notice a.wcpay-inline-notice__action{text-decoration:none}.wcpay-inline-notice.components-notice.is-dismissible{padding-right:12px}.wcpay-inline-notice.components-notice .components-notice__dismiss{height:24px;width:24px}.wcpay-inline-notice.components-notice .components-notice__dismiss svg{width:15px;height:15px;fill:#1e1e1e}.wcpay-inline-notice.components-notice.is-info{background-color:#f0f6fc}.wcpay-inline-notice.components-notice.is-info .wcpay-inline-notice__icon svg{fill:#0a4b78}.wcpay-inline-notice.components-notice.is-info button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #0a4b78}.wcpay-inline-notice.components-notice.is-info .wcpay-inline-notice__action{color:#0a4b78}.wcpay-inline-notice.components-notice.is-warning{background-color:#fcf9e8}.wcpay-inline-notice.components-notice.is-warning .wcpay-inline-notice__icon svg{fill:#614200}.wcpay-inline-notice.components-notice.is-warning button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #614200}.wcpay-inline-notice.components-notice.is-warning .wcpay-inline-notice__action{color:#614200}.wcpay-inline-notice.components-notice.is-error{background-color:#fcf0f1}.wcpay-inline-notice.components-notice.is-error .wcpay-inline-notice__icon svg{fill:#8a2424}.wcpay-inline-notice.components-notice.is-error button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #8a2424}.wcpay-inline-notice.components-notice.is-error .wcpay-inline-notice__action{color:#8a2424}.wcpay-inline-notice.components-notice.is-success{background-color:#edfaef}.wcpay-inline-notice.components-notice.is-success .wcpay-inline-notice__icon svg{fill:#005c12}.wcpay-inline-notice.components-notice.is-success button.wcpay-inline-notice__action{box-shadow:inset 0 0 0 1px #005c12}.wcpay-inline-notice.components-notice.is-success .wcpay-inline-notice__action{color:#005c12}.wcpay-inline-notice.components-notice button.wcpay-inline-notice__action.is-link{text-decoration:none;box-shadow:none} .woocommerce-payments-page{margin:0 auto}.woocommerce-payments-page.is-narrow{max-width:680px} .is-loadable-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#e2e4e7;color:transparent;display:inline-block}@media screen and (prefers-reduced-motion: reduce){.is-loadable-placeholder{animation:none}}.is-loadable-placeholder.is-inline{display:inline}.is-loadable-placeholder.is-block{display:block;margin-top:1em}.is-loadable-placeholder.is-block:first-child{margin-top:0}.is-loadable-placeholder.is-block p{margin:0} .woocommerce-payments__payment-method-icon{display:inline-flex;align-items:center;vertical-align:middle;max-width:160px}@media(min-width: 661px){.woocommerce-payments__payment-method-icon{max-width:inherit}}.woocommerce-payments__payment-method-icon__label{margin-left:4px}.woocommerce-payments__payment-method-icon svg{width:38px;height:auto}.woocommerce-payments__payment-method-icon .payment-method__icon{order:initial;margin-right:3px;max-width:45px} .wcpay-settings-layout{margin:24px auto 0;max-width:1032px;display:flex;flex-flow:column}@media(min-width: 961px){.wcpay-settings-layout{padding:0 56px}}body.woocommerce-payments-checkout-section .wcpay-settings-layout{margin:24px 0 0;max-width:1184px}@media(min-width: 961px){body.woocommerce-payments-checkout-section .wcpay-settings-layout{padding:0 30px}} .settings-section{display:flex;flex-flow:column}.settings-section:not(.settings-section:last-child){margin-bottom:70px}@media(min-width: 801px){.settings-section{flex-flow:row}}.settings-section__details{flex:0 1 auto;margin-bottom:24px;max-width:100%}@media(min-width: 801px){.settings-section__details{flex:0 0 33%;margin:0 24px 0 0;max-width:328px}}.settings-section__details h2{font-size:var(--l, 15px);font-style:normal;font-weight:var(--Medium, 500);line-height:var(--s, 20px);font-size:16px;line-height:24px;margin-top:0}.settings-section__details p{font-size:13px;line-height:17.89px;margin:12px 0}.settings-section__details>:last-child{margin-bottom:0}.settings-section__icons>li{display:inline-block;margin:0 5px 0 0}.settings-section__icons svg{border-radius:2px;box-shadow:0 0 0 1px #ddd;height:auto;width:38px}.settings-section__controls{flex:1 1 auto}.settings-section__controls .components-radio-control:not(:last-child),.settings-section__controls .components-base-control:not(:last-child){margin-bottom:24px} .multi-currency-store-settings-preview-modal{width:calc(100vw - 64px);min-width:calc(100vw - 64px);min-height:calc(100vh - 64px);display:flex;flex-direction:column}.multi-currency-store-settings-preview-modal .components-modal__content{padding:0;flex:1;display:flex;flex-direction:column}.multi-currency-store-settings-preview-modal .components-modal__content .components-modal__header{margin:0;padding:0 32px}.multi-currency-store-settings-preview-modal .components-modal__content iframe{flex:1;width:100%;height:100%} .multi-currency-settings-task__wrapper{margin-top:24px;display:flex;flex-direction:column;gap:16px} .setup-complete-task__buttons .components-button:not(:last-child){margin-right:8px}.setup-complete-task__enabled-methods-list{display:flex;align-items:center}.setup-complete-task__enabled-methods-list>*:not(:last-child){margin-right:8px} .add-currencies-task__currency-selector-wrapper.components-card .components-card__body{padding-left:0;padding-right:0}.add-currencies-task__currency-selector-wrapper.components-card .components-card__body h4,.add-currencies-task__currency-selector-wrapper.components-card .components-card__body .add-currencies-task__content .enabled-currency-checkbox,.add-currencies-task__currency-selector-wrapper.components-card .components-card__body .add-currencies-task__search{padding-left:16px;padding-right:16px}.add-currencies-task__currency-selector-wrapper.components-card .components-card__body .is-loadable-placeholder{margin-left:16px;margin-right:16px}.add-currencies-task__separator{border-bottom:1px solid #dcdcde;font-size:.1px;width:100%;background-color:#fff;height:1px;padding:16px 0 0 0;margin:0}.add-currencies-task__content{max-height:400px;min-height:400px;overflow-y:auto}.add-currencies-task__content .enabled-currency-checkbox{margin-top:-5px;padding-top:5px}.add-currencies-task__content .enabled-currency-checkbox__flag{font-size:20px;margin:0 5px 0 0}.add-currencies-task__content .enabled-currency-checkbox__flag-text{display:inline-block;width:20px;height:15px;line-height:15px;vertical-align:top;margin:0 .07em;background-color:#dcdcde;border-radius:2px;text-align:center}.add-currencies-task__content .enabled-currency-checkbox__flag-text span{font-size:9px;vertical-align:bottom}.add-currencies-task__content .enabled-currency-checkbox__code{color:#8c8f94}.add-currencies-task__content .components-checkbox-control__label{font-size:15px} .multi-currency-setup-wizard{max-width:700px;min-width:510px;margin:0 auto}.multi-currency-setup-wizard strong{font-weight:500} .multi-currency-settings .settings-section:not(.settings-section:last-child).multi-currency-settings-enabled-currencies-section{margin-bottom:32px}.wcpay-multi-currency__enabled-currencies-header{display:flex}.wcpay-multi-currency__enabled-currencies-header div{flex:1}.wcpay-multi-currency__enabled-currencies-header div:nth-child(1){min-width:45%}.wcpay-multi-currency__enabled-currencies-header div:nth-child(3){min-width:10%}.enabled-currencies-list{margin:0}.enabled-currencies-list .enabled-currency{display:flex;flex-wrap:wrap;padding:7px 24px;margin:0;border-top:1px solid #dcdcde;line-height:45px}.enabled-currencies-list .enabled-currency:first-child{border-top:none;background-color:#f6f7f7}.enabled-currencies-list .enabled-currency__container{display:flex;flex-wrap:wrap;min-width:45%}.enabled-currencies-list .enabled-currency__container,.enabled-currencies-list .enabled-currency__rate{flex:1}.enabled-currencies-list .enabled-currency__flag{font-size:24px;margin:0 10px 0 0}.enabled-currencies-list .enabled-currency__flag-text{width:24px;height:18px;line-height:18px;margin:14px .07em;background-color:#dcdcde;border-radius:5px;text-align:center}.enabled-currencies-list .enabled-currency__flag-text span{font-size:9px;vertical-align:bottom}.enabled-currencies-list .enabled-currency__label{padding:0 5px 0 0}.enabled-currencies-list .enabled-currency__code,.enabled-currencies-list .enabled-currency__rate{color:#8c8f94}.enabled-currencies-list .enabled-currency__actions{text-align:right;display:flex;min-width:10%;color:#a7aaad;justify-content:right}.enabled-currencies-list .enabled-currency__actions .components-button{text-decoration:none;color:#a7aaad}.enabled-currencies-list .enabled-currency__actions .delete{margin:0 0 0 24px;padding:0 0 0 5px}.enabled-currencies-list .enabled-currency-placeholder{align-items:center}.enabled-currencies-list .enabled-currency-placeholder p{white-space:nowrap;overflow:hidden}.enabled-currencies-list .enabled-currency-placeholder .is-block{margin:10px 0 10px !important}.enabled-currencies-list .enabled-currency-placeholder .enabled-currency__container{align-items:center}.enabled-currencies-list .enabled-currency-placeholder .enabled-currency__container .enabled-currency__flag{width:28px}.enabled-currencies-list .enabled-currency-placeholder .enabled-currency__container .enabled-currency__flag p{font-size:24px}.enabled-currencies-list .enabled-currency-placeholder .enabled-currency__container .enabled-currency__label{width:120px}.enabled-currencies-list .enabled-currency-placeholder .enabled-currency__container .enabled-currency__code{width:40px}.enabled-currencies-list .enabled-currency-placeholder .enabled-currency__rate .is-block{width:150px}.enabled-currencies-list .enabled-currency-placeholder .enabled-currency__actions{justify-content:end}.enabled-currencies-list .enabled-currency-placeholder .enabled-currency__actions p{width:100px}.add-enabled-currencies-modal{max-width:500px}.add-enabled-currencies-modal .enabled-currency-checkbox__flag{font-size:20px;margin:0 5px 0 0}.add-enabled-currencies-modal .enabled-currency-checkbox__flag-text{display:inline-block;width:20px;height:15px;line-height:15px;vertical-align:top;margin:0 .07em;background-color:#dcdcde;border-radius:2px;text-align:center}.add-enabled-currencies-modal .enabled-currency-checkbox__flag-text span{font-size:9px;vertical-align:bottom}.add-enabled-currencies-modal .enabled-currency-checkbox__code{color:#8c8f94}.add-enabled-currencies-modal .components-checkbox-control__label{font-size:15px}.add-enabled-currencies-modal__content{overflow-y:scroll;flex:1;display:flex;flex-direction:column;margin:0 -24px -24px 0;padding-left:4px;max-height:350px}.add-enabled-currencies-modal__search{border-bottom:1px solid #dcdcde;margin:-16px -24px 0;padding:16px}.add-enabled-currencies-modal__search .search{width:auto;height:auto;margin:0;padding:0}.wcpay-confirmation-modal.wcpay-confirmation-modal.enabled-currency-delete-modal{max-width:495px}.wcpay-confirmation-modal.wcpay-confirmation-modal.enabled-currency-delete-modal .components-modal__content{padding:0 16px 16px}.wcpay-confirmation-modal.wcpay-confirmation-modal.enabled-currency-delete-modal .components-modal__content p{font-size:14px;line-height:19px}.wcpay-confirmation-modal.wcpay-confirmation-modal.enabled-currency-delete-modal .components-modal__content ul{margin-block-start:14px;margin-block-end:22px}.wcpay-confirmation-modal.wcpay-confirmation-modal.enabled-currency-delete-modal .components-modal__content ul li{margin-bottom:12px}.wcpay-confirmation-modal.wcpay-confirmation-modal.enabled-currency-delete-modal .components-modal__content ul li .woocommerce-payments__payment-method-icon svg{margin-right:8px}.wcpay-confirmation-modal.wcpay-confirmation-modal.enabled-currency-delete-modal .wcpay-confirmation-modal__separator{margin:32px -16px 16px}.wcpay-confirmation-modal.wcpay-confirmation-modal.enabled-currency-delete-modal .wcpay-confirmation-modal__footer>*:not(:first-child){margin-left:12px}@media screen and (max-width: 600px){.add-enabled-currencies-modal .components-modal__content{display:flex;flex-direction:column}.add-enabled-currencies-modal__content{max-height:none;min-height:0;flex:1}} .currency-delete-illustration__wrapper{margin:30px auto 24px auto;width:35%}.currency-delete-illustration__illustrations{position:relative;margin:0 auto;background-color:#000580;color:#ffe600;width:96px;height:72px;display:flex;align-items:center;justify-content:center;font-size:48px;line-height:72px}.currency-delete-illustration__currency-cross-icon{position:absolute;top:-17px;right:-17px;width:34px;height:34px;fill:#cc1818;background:#fff;border-radius:999px} @media screen and (max-width: 782px){.woocommerce-layout .woocommerce-layout__main{padding-top:16px;padding-bottom:16px}}.woocommerce-payments-page .components-card__body>*:first-child{margin-top:0}.woocommerce-payments-page .components-card__body>*:last-child{margin-bottom:0}.woocommerce-payments-page .woocommerce-table .woocommerce-table__item{white-space:nowrap}.woocommerce-payments-page .woocommerce-table .info-button{width:40px;padding-right:0}.woocommerce-payments-page .woocommerce-table .info-button>a{height:18px;fill:#8c8f94}.woocommerce-payments-page .woocommerce-table .info-button+.woocommerce-table__header{border-left:none}.woocommerce-payments-page .woocommerce-table .info-button .is-placeholder{display:none}.woocommerce-payments-page .woocommerce-table .payment-method__brand{display:block;margin:0 auto;text-align:center}.wcpay-test-mode-notice.components-notice.is-warning{margin:24px 0;padding:12px;background-color:#f5e6b3}.wcpay-test-mode-notice.components-notice.is-informational{margin:24px 0;padding:12px}.wcpay-connect-error-notice.components-notice{margin:24px 0;padding:12px}.payment-method-details{display:flex;align-items:flex-start}@media screen and (max-width: 1023px){.payment-method-details{display:block}}.payment-method-details__column{flex:0 0 50%}.payment-method-details-list-item{display:inline-flex}.payment-method-detail{display:flex;align-items:flex-start;font-size:14px;line-height:28px}.payment-method-detail__label{margin:0;color:#50575e;flex:0 0 36%;font-weight:normal;line-height:20px;padding:4px 0}@media screen and (max-width: 470px){.payment-method-detail__label{flex:0 0 40%}}.payment-method-detail__value{margin:0;color:#2c3338;line-height:20px;padding:4px 0;word-break:break-word}.components-modal__content .components-modal__header .components-button .components-tooltip{display:none}img.payment-method__icon{border:1px solid rgba(0,0,0,.25);border-radius:4px;display:block}img.payment-method__icon.no-border{border:none;border-radius:0}@media screen and (min-width: 783px){.woocommerce-filters-deposits .woocommerce-filters-advanced__fieldset,.woocommerce-filters-disputes .woocommerce-filters-advanced__fieldset,.woocommerce-filters-documents .woocommerce-filters-advanced__fieldset,.woocommerce-filters-transactions .woocommerce-filters-advanced__fieldset{display:grid;grid-template-columns:100px auto 1fr}.woocommerce-filters-deposits .components-select-control .components-input-control__container .components-select-control__input,.woocommerce-filters-disputes .components-select-control .components-input-control__container .components-select-control__input,.woocommerce-filters-documents .components-select-control .components-input-control__container .components-select-control__input,.woocommerce-filters-transactions .components-select-control .components-input-control__container .components-select-control__input{padding-right:var(--main-gap)}} .multi-currency-settings__wrapper .components-card__body{padding:24px}.multi-currency-settings__wrapper .components-card__body .components-base-control .components-base-control__field{margin-bottom:0}.multi-currency-settings .multi-currency-settings-save-settings-section{text-align:right} .single-currency-settings .single-currency-settings-breadcrumb{font-style:normal;font-weight:600;font-size:16px;line-height:24px;color:#000}#mainform .single-currency-settings .single-currency-settings-breadcrumb{margin-bottom:30px}.single-currency-settings .single-currency-settings-breadcrumb button{font-size:16px;font-weight:600}.single-currency-settings .settings-section__controls .single-currency-settings-currency-settings fieldset{margin:0 0 24px}.single-currency-settings .settings-section__controls .single-currency-settings-currency-settings fieldset:last-child{margin:0}.single-currency-settings .settings-section__controls .single-currency-settings-currency-settings fieldset ul{margin:0;font-size:13px}.single-currency-settings .settings-section__controls .single-currency-settings-currency-settings fieldset ul li{margin:0 0 16px}.single-currency-settings .settings-section__controls .single-currency-settings-currency-settings fieldset ul li:last-child{margin:0}.single-currency-settings .single-currency-settings-preview-wrapper .wcpay-card-body{display:flex}.single-currency-settings .single-currency-settings-preview-wrapper .wcpay-card-body>div{padding:0 6px 0 0;margin-bottom:0;flex:1 0 calc(50% - 6px);display:flex;flex-direction:column}.single-currency-settings .single-currency-settings-preview-wrapper .wcpay-card-body>div:nth-child(2n){padding:0 0 0 6px}.single-currency-settings .single-currency-settings-preview-wrapper .wcpay-card-body>div>div{display:flex;align-items:center;justify-content:stretch;flex:1}.single-currency-settings .single-currency-settings-preview-wrapper .wcpay-card-body>div>div .components-base-control{flex:1}.single-currency-settings .single-currency-settings-preview-wrapper .wcpay-card-body>div>div .components-base-control .components-base-control__field{margin:0}.single-currency-settings .single-currency-settings-preview-wrapper .wcpay-card-body>div>div .components-base-control .components-base-control__field .text-control-with-affixes.disabled input{border:none;color:initial;background:unset;font-weight:bold}.single-currency-settings .single-currency-settings-preview-wrapper .wcpay-card-body>div>div .components-base-control .components-base-control__field .text-control-with-affixes>span{border-color:#949494;box-sizing:border-box;font-style:normal;font-weight:normal;font-size:13px;line-height:16px;display:flex;align-items:center;padding:7px 12px}.single-currency-settings .single-currency-settings-save-settings-section{text-align:right} dist/settings.asset.php 0000777 00000000606 15251156605 0011215 0 ustar 00 <?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wc-components', 'wc-currency', 'wc-navigation', 'wc-number', 'wc-settings', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-polyfill', 'wp-primitives', 'wp-url'), 'version' => 'afd29e8fff24bc00f41b'); dist/woopay-direct-checkout.js.LICENSE.txt 0000777 00000000371 15251156605 0014353 0 ustar 00 /** * @license React * react-jsx-runtime.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ dist/woopay.css 0000777 00000073055 15251156605 0007566 0 ustar 00 .iti { position: relative; display: inline-block; } .iti * { box-sizing: border-box; -moz-box-sizing: border-box; } .iti__hide { display: none; } .iti__v-hide { visibility: hidden; } .iti input, .iti input[type=text], .iti input[type=tel] { position: relative; z-index: 0; margin-top: 0 !important; margin-bottom: 0 !important; padding-right: 36px; margin-right: 0; } .iti__flag-container { position: absolute; top: 0; bottom: 0; right: 0; padding: 1px; } .iti__selected-flag { z-index: 1; position: relative; display: flex; align-items: center; height: 100%; padding: 0 6px 0 8px; } .iti__arrow { margin-left: 6px; width: 0; height: 0; border-left: 3px solid transparent; border-right: 3px solid transparent; border-top: 4px solid #555; } .iti__arrow--up { border-top: none; border-bottom: 4px solid #555; } .iti__country-list { position: absolute; z-index: 2; list-style: none; text-align: left; padding: 0; margin: 0 0 0 -1px; box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2); background-color: white; border: 1px solid #CCC; white-space: nowrap; max-height: 200px; overflow-y: scroll; -webkit-overflow-scrolling: touch; } .iti__country-list--dropup { bottom: 100%; margin-bottom: -1px; } @media (max-width: 500px) { .iti__country-list { white-space: normal; } } .iti__flag-box { display: inline-block; width: 20px; } .iti__divider { padding-bottom: 5px; margin-bottom: 5px; border-bottom: 1px solid #CCC; } .iti__country { padding: 5px 10px; outline: none; } .iti__dial-code { color: #999; } .iti__country.iti__highlight { background-color: rgba(0, 0, 0, 0.05); } .iti__flag-box, .iti__country-name, .iti__dial-code { vertical-align: middle; } .iti__flag-box, .iti__country-name { margin-right: 6px; } .iti--allow-dropdown input, .iti--allow-dropdown input[type=text], .iti--allow-dropdown input[type=tel], .iti--separate-dial-code input, .iti--separate-dial-code input[type=text], .iti--separate-dial-code input[type=tel] { padding-right: 6px; padding-left: 52px; margin-left: 0; } .iti--allow-dropdown .iti__flag-container, .iti--separate-dial-code .iti__flag-container { right: auto; left: 0; } .iti--allow-dropdown .iti__flag-container:hover { cursor: pointer; } .iti--allow-dropdown .iti__flag-container:hover .iti__selected-flag { background-color: rgba(0, 0, 0, 0.05); } .iti--allow-dropdown input[disabled] + .iti__flag-container:hover, .iti--allow-dropdown input[readonly] + .iti__flag-container:hover { cursor: default; } .iti--allow-dropdown input[disabled] + .iti__flag-container:hover .iti__selected-flag, .iti--allow-dropdown input[readonly] + .iti__flag-container:hover .iti__selected-flag { background-color: transparent; } .iti--separate-dial-code .iti__selected-flag { background-color: rgba(0, 0, 0, 0.05); } .iti--separate-dial-code .iti__selected-dial-code { margin-left: 6px; } .iti--container { position: absolute; top: -1000px; left: -1000px; z-index: 1060; padding: 1px; } .iti--container:hover { cursor: pointer; } .iti-mobile .iti--container { top: 30px; bottom: 30px; left: 30px; right: 30px; position: fixed; } .iti-mobile .iti__country-list { max-height: 100%; width: 100%; } .iti-mobile .iti__country { padding: 10px 10px; line-height: 1.5em; } .iti__flag { width: 20px; } .iti__flag.iti__be { width: 18px; } .iti__flag.iti__ch { width: 15px; } .iti__flag.iti__mc { width: 19px; } .iti__flag.iti__ne { width: 18px; } .iti__flag.iti__np { width: 13px; } .iti__flag.iti__va { width: 15px; } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .iti__flag { background-size: 5652px 15px; } } .iti__flag.iti__ac { height: 10px; background-position: 0px 0px; } .iti__flag.iti__ad { height: 14px; background-position: -22px 0px; } .iti__flag.iti__ae { height: 10px; background-position: -44px 0px; } .iti__flag.iti__af { height: 14px; background-position: -66px 0px; } .iti__flag.iti__ag { height: 14px; background-position: -88px 0px; } .iti__flag.iti__ai { height: 10px; background-position: -110px 0px; } .iti__flag.iti__al { height: 15px; background-position: -132px 0px; } .iti__flag.iti__am { height: 10px; background-position: -154px 0px; } .iti__flag.iti__ao { height: 14px; background-position: -176px 0px; } .iti__flag.iti__aq { height: 14px; background-position: -198px 0px; } .iti__flag.iti__ar { height: 13px; background-position: -220px 0px; } .iti__flag.iti__as { height: 10px; background-position: -242px 0px; } .iti__flag.iti__at { height: 14px; background-position: -264px 0px; } .iti__flag.iti__au { height: 10px; background-position: -286px 0px; } .iti__flag.iti__aw { height: 14px; background-position: -308px 0px; } .iti__flag.iti__ax { height: 13px; background-position: -330px 0px; } .iti__flag.iti__az { height: 10px; background-position: -352px 0px; } .iti__flag.iti__ba { height: 10px; background-position: -374px 0px; } .iti__flag.iti__bb { height: 14px; background-position: -396px 0px; } .iti__flag.iti__bd { height: 12px; background-position: -418px 0px; } .iti__flag.iti__be { height: 15px; background-position: -440px 0px; } .iti__flag.iti__bf { height: 14px; background-position: -460px 0px; } .iti__flag.iti__bg { height: 12px; background-position: -482px 0px; } .iti__flag.iti__bh { height: 12px; background-position: -504px 0px; } .iti__flag.iti__bi { height: 12px; background-position: -526px 0px; } .iti__flag.iti__bj { height: 14px; background-position: -548px 0px; } .iti__flag.iti__bl { height: 14px; background-position: -570px 0px; } .iti__flag.iti__bm { height: 10px; background-position: -592px 0px; } .iti__flag.iti__bn { height: 10px; background-position: -614px 0px; } .iti__flag.iti__bo { height: 14px; background-position: -636px 0px; } .iti__flag.iti__bq { height: 14px; background-position: -658px 0px; } .iti__flag.iti__br { height: 14px; background-position: -680px 0px; } .iti__flag.iti__bs { height: 10px; background-position: -702px 0px; } .iti__flag.iti__bt { height: 14px; background-position: -724px 0px; } .iti__flag.iti__bv { height: 15px; background-position: -746px 0px; } .iti__flag.iti__bw { height: 14px; background-position: -768px 0px; } .iti__flag.iti__by { height: 10px; background-position: -790px 0px; } .iti__flag.iti__bz { height: 14px; background-position: -812px 0px; } .iti__flag.iti__ca { height: 10px; background-position: -834px 0px; } .iti__flag.iti__cc { height: 10px; background-position: -856px 0px; } .iti__flag.iti__cd { height: 15px; background-position: -878px 0px; } .iti__flag.iti__cf { height: 14px; background-position: -900px 0px; } .iti__flag.iti__cg { height: 14px; background-position: -922px 0px; } .iti__flag.iti__ch { height: 15px; background-position: -944px 0px; } .iti__flag.iti__ci { height: 14px; background-position: -961px 0px; } .iti__flag.iti__ck { height: 10px; background-position: -983px 0px; } .iti__flag.iti__cl { height: 14px; background-position: -1005px 0px; } .iti__flag.iti__cm { height: 14px; background-position: -1027px 0px; } .iti__flag.iti__cn { height: 14px; background-position: -1049px 0px; } .iti__flag.iti__co { height: 14px; background-position: -1071px 0px; } .iti__flag.iti__cp { height: 14px; background-position: -1093px 0px; } .iti__flag.iti__cr { height: 12px; background-position: -1115px 0px; } .iti__flag.iti__cu { height: 10px; background-position: -1137px 0px; } .iti__flag.iti__cv { height: 12px; background-position: -1159px 0px; } .iti__flag.iti__cw { height: 14px; background-position: -1181px 0px; } .iti__flag.iti__cx { height: 10px; background-position: -1203px 0px; } .iti__flag.iti__cy { height: 14px; background-position: -1225px 0px; } .iti__flag.iti__cz { height: 14px; background-position: -1247px 0px; } .iti__flag.iti__de { height: 12px; background-position: -1269px 0px; } .iti__flag.iti__dg { height: 10px; background-position: -1291px 0px; } .iti__flag.iti__dj { height: 14px; background-position: -1313px 0px; } .iti__flag.iti__dk { height: 15px; background-position: -1335px 0px; } .iti__flag.iti__dm { height: 10px; background-position: -1357px 0px; } .iti__flag.iti__do { height: 14px; background-position: -1379px 0px; } .iti__flag.iti__dz { height: 14px; background-position: -1401px 0px; } .iti__flag.iti__ea { height: 14px; background-position: -1423px 0px; } .iti__flag.iti__ec { height: 14px; background-position: -1445px 0px; } .iti__flag.iti__ee { height: 13px; background-position: -1467px 0px; } .iti__flag.iti__eg { height: 14px; background-position: -1489px 0px; } .iti__flag.iti__eh { height: 10px; background-position: -1511px 0px; } .iti__flag.iti__er { height: 10px; background-position: -1533px 0px; } .iti__flag.iti__es { height: 14px; background-position: -1555px 0px; } .iti__flag.iti__et { height: 10px; background-position: -1577px 0px; } .iti__flag.iti__eu { height: 14px; background-position: -1599px 0px; } .iti__flag.iti__fi { height: 12px; background-position: -1621px 0px; } .iti__flag.iti__fj { height: 10px; background-position: -1643px 0px; } .iti__flag.iti__fk { height: 10px; background-position: -1665px 0px; } .iti__flag.iti__fm { height: 11px; background-position: -1687px 0px; } .iti__flag.iti__fo { height: 15px; background-position: -1709px 0px; } .iti__flag.iti__fr { height: 14px; background-position: -1731px 0px; } .iti__flag.iti__ga { height: 15px; background-position: -1753px 0px; } .iti__flag.iti__gb { height: 10px; background-position: -1775px 0px; } .iti__flag.iti__gd { height: 12px; background-position: -1797px 0px; } .iti__flag.iti__ge { height: 14px; background-position: -1819px 0px; } .iti__flag.iti__gf { height: 14px; background-position: -1841px 0px; } .iti__flag.iti__gg { height: 14px; background-position: -1863px 0px; } .iti__flag.iti__gh { height: 14px; background-position: -1885px 0px; } .iti__flag.iti__gi { height: 10px; background-position: -1907px 0px; } .iti__flag.iti__gl { height: 14px; background-position: -1929px 0px; } .iti__flag.iti__gm { height: 14px; background-position: -1951px 0px; } .iti__flag.iti__gn { height: 14px; background-position: -1973px 0px; } .iti__flag.iti__gp { height: 14px; background-position: -1995px 0px; } .iti__flag.iti__gq { height: 14px; background-position: -2017px 0px; } .iti__flag.iti__gr { height: 14px; background-position: -2039px 0px; } .iti__flag.iti__gs { height: 10px; background-position: -2061px 0px; } .iti__flag.iti__gt { height: 13px; background-position: -2083px 0px; } .iti__flag.iti__gu { height: 11px; background-position: -2105px 0px; } .iti__flag.iti__gw { height: 10px; background-position: -2127px 0px; } .iti__flag.iti__gy { height: 12px; background-position: -2149px 0px; } .iti__flag.iti__hk { height: 14px; background-position: -2171px 0px; } .iti__flag.iti__hm { height: 10px; background-position: -2193px 0px; } .iti__flag.iti__hn { height: 10px; background-position: -2215px 0px; } .iti__flag.iti__hr { height: 10px; background-position: -2237px 0px; } .iti__flag.iti__ht { height: 12px; background-position: -2259px 0px; } .iti__flag.iti__hu { height: 10px; background-position: -2281px 0px; } .iti__flag.iti__ic { height: 14px; background-position: -2303px 0px; } .iti__flag.iti__id { height: 14px; background-position: -2325px 0px; } .iti__flag.iti__ie { height: 10px; background-position: -2347px 0px; } .iti__flag.iti__il { height: 15px; background-position: -2369px 0px; } .iti__flag.iti__im { height: 10px; background-position: -2391px 0px; } .iti__flag.iti__in { height: 14px; background-position: -2413px 0px; } .iti__flag.iti__io { height: 10px; background-position: -2435px 0px; } .iti__flag.iti__iq { height: 14px; background-position: -2457px 0px; } .iti__flag.iti__ir { height: 12px; background-position: -2479px 0px; } .iti__flag.iti__is { height: 15px; background-position: -2501px 0px; } .iti__flag.iti__it { height: 14px; background-position: -2523px 0px; } .iti__flag.iti__je { height: 12px; background-position: -2545px 0px; } .iti__flag.iti__jm { height: 10px; background-position: -2567px 0px; } .iti__flag.iti__jo { height: 10px; background-position: -2589px 0px; } .iti__flag.iti__jp { height: 14px; background-position: -2611px 0px; } .iti__flag.iti__ke { height: 14px; background-position: -2633px 0px; } .iti__flag.iti__kg { height: 12px; background-position: -2655px 0px; } .iti__flag.iti__kh { height: 13px; background-position: -2677px 0px; } .iti__flag.iti__ki { height: 10px; background-position: -2699px 0px; } .iti__flag.iti__km { height: 12px; background-position: -2721px 0px; } .iti__flag.iti__kn { height: 14px; background-position: -2743px 0px; } .iti__flag.iti__kp { height: 10px; background-position: -2765px 0px; } .iti__flag.iti__kr { height: 14px; background-position: -2787px 0px; } .iti__flag.iti__kw { height: 10px; background-position: -2809px 0px; } .iti__flag.iti__ky { height: 10px; background-position: -2831px 0px; } .iti__flag.iti__kz { height: 10px; background-position: -2853px 0px; } .iti__flag.iti__la { height: 14px; background-position: -2875px 0px; } .iti__flag.iti__lb { height: 14px; background-position: -2897px 0px; } .iti__flag.iti__lc { height: 10px; background-position: -2919px 0px; } .iti__flag.iti__li { height: 12px; background-position: -2941px 0px; } .iti__flag.iti__lk { height: 10px; background-position: -2963px 0px; } .iti__flag.iti__lr { height: 11px; background-position: -2985px 0px; } .iti__flag.iti__ls { height: 14px; background-position: -3007px 0px; } .iti__flag.iti__lt { height: 12px; background-position: -3029px 0px; } .iti__flag.iti__lu { height: 12px; background-position: -3051px 0px; } .iti__flag.iti__lv { height: 10px; background-position: -3073px 0px; } .iti__flag.iti__ly { height: 10px; background-position: -3095px 0px; } .iti__flag.iti__ma { height: 14px; background-position: -3117px 0px; } .iti__flag.iti__mc { height: 15px; background-position: -3139px 0px; } .iti__flag.iti__md { height: 10px; background-position: -3160px 0px; } .iti__flag.iti__me { height: 10px; background-position: -3182px 0px; } .iti__flag.iti__mf { height: 14px; background-position: -3204px 0px; } .iti__flag.iti__mg { height: 14px; background-position: -3226px 0px; } .iti__flag.iti__mh { height: 11px; background-position: -3248px 0px; } .iti__flag.iti__mk { height: 10px; background-position: -3270px 0px; } .iti__flag.iti__ml { height: 14px; background-position: -3292px 0px; } .iti__flag.iti__mm { height: 14px; background-position: -3314px 0px; } .iti__flag.iti__mn { height: 10px; background-position: -3336px 0px; } .iti__flag.iti__mo { height: 14px; background-position: -3358px 0px; } .iti__flag.iti__mp { height: 10px; background-position: -3380px 0px; } .iti__flag.iti__mq { height: 14px; background-position: -3402px 0px; } .iti__flag.iti__mr { height: 14px; background-position: -3424px 0px; } .iti__flag.iti__ms { height: 10px; background-position: -3446px 0px; } .iti__flag.iti__mt { height: 14px; background-position: -3468px 0px; } .iti__flag.iti__mu { height: 14px; background-position: -3490px 0px; } .iti__flag.iti__mv { height: 14px; background-position: -3512px 0px; } .iti__flag.iti__mw { height: 14px; background-position: -3534px 0px; } .iti__flag.iti__mx { height: 12px; background-position: -3556px 0px; } .iti__flag.iti__my { height: 10px; background-position: -3578px 0px; } .iti__flag.iti__mz { height: 14px; background-position: -3600px 0px; } .iti__flag.iti__na { height: 14px; background-position: -3622px 0px; } .iti__flag.iti__nc { height: 10px; background-position: -3644px 0px; } .iti__flag.iti__ne { height: 15px; background-position: -3666px 0px; } .iti__flag.iti__nf { height: 10px; background-position: -3686px 0px; } .iti__flag.iti__ng { height: 10px; background-position: -3708px 0px; } .iti__flag.iti__ni { height: 12px; background-position: -3730px 0px; } .iti__flag.iti__nl { height: 14px; background-position: -3752px 0px; } .iti__flag.iti__no { height: 15px; background-position: -3774px 0px; } .iti__flag.iti__np { height: 15px; background-position: -3796px 0px; } .iti__flag.iti__nr { height: 10px; background-position: -3811px 0px; } .iti__flag.iti__nu { height: 10px; background-position: -3833px 0px; } .iti__flag.iti__nz { height: 10px; background-position: -3855px 0px; } .iti__flag.iti__om { height: 10px; background-position: -3877px 0px; } .iti__flag.iti__pa { height: 14px; background-position: -3899px 0px; } .iti__flag.iti__pe { height: 14px; background-position: -3921px 0px; } .iti__flag.iti__pf { height: 14px; background-position: -3943px 0px; } .iti__flag.iti__pg { height: 15px; background-position: -3965px 0px; } .iti__flag.iti__ph { height: 10px; background-position: -3987px 0px; } .iti__flag.iti__pk { height: 14px; background-position: -4009px 0px; } .iti__flag.iti__pl { height: 13px; background-position: -4031px 0px; } .iti__flag.iti__pm { height: 14px; background-position: -4053px 0px; } .iti__flag.iti__pn { height: 10px; background-position: -4075px 0px; } .iti__flag.iti__pr { height: 14px; background-position: -4097px 0px; } .iti__flag.iti__ps { height: 10px; background-position: -4119px 0px; } .iti__flag.iti__pt { height: 14px; background-position: -4141px 0px; } .iti__flag.iti__pw { height: 13px; background-position: -4163px 0px; } .iti__flag.iti__py { height: 11px; background-position: -4185px 0px; } .iti__flag.iti__qa { height: 8px; background-position: -4207px 0px; } .iti__flag.iti__re { height: 14px; background-position: -4229px 0px; } .iti__flag.iti__ro { height: 14px; background-position: -4251px 0px; } .iti__flag.iti__rs { height: 14px; background-position: -4273px 0px; } .iti__flag.iti__ru { height: 14px; background-position: -4295px 0px; } .iti__flag.iti__rw { height: 14px; background-position: -4317px 0px; } .iti__flag.iti__sa { height: 14px; background-position: -4339px 0px; } .iti__flag.iti__sb { height: 10px; background-position: -4361px 0px; } .iti__flag.iti__sc { height: 10px; background-position: -4383px 0px; } .iti__flag.iti__sd { height: 10px; background-position: -4405px 0px; } .iti__flag.iti__se { height: 13px; background-position: -4427px 0px; } .iti__flag.iti__sg { height: 14px; background-position: -4449px 0px; } .iti__flag.iti__sh { height: 10px; background-position: -4471px 0px; } .iti__flag.iti__si { height: 10px; background-position: -4493px 0px; } .iti__flag.iti__sj { height: 15px; background-position: -4515px 0px; } .iti__flag.iti__sk { height: 14px; background-position: -4537px 0px; } .iti__flag.iti__sl { height: 14px; background-position: -4559px 0px; } .iti__flag.iti__sm { height: 15px; background-position: -4581px 0px; } .iti__flag.iti__sn { height: 14px; background-position: -4603px 0px; } .iti__flag.iti__so { height: 14px; background-position: -4625px 0px; } .iti__flag.iti__sr { height: 14px; background-position: -4647px 0px; } .iti__flag.iti__ss { height: 10px; background-position: -4669px 0px; } .iti__flag.iti__st { height: 10px; background-position: -4691px 0px; } .iti__flag.iti__sv { height: 12px; background-position: -4713px 0px; } .iti__flag.iti__sx { height: 14px; background-position: -4735px 0px; } .iti__flag.iti__sy { height: 14px; background-position: -4757px 0px; } .iti__flag.iti__sz { height: 14px; background-position: -4779px 0px; } .iti__flag.iti__ta { height: 10px; background-position: -4801px 0px; } .iti__flag.iti__tc { height: 10px; background-position: -4823px 0px; } .iti__flag.iti__td { height: 14px; background-position: -4845px 0px; } .iti__flag.iti__tf { height: 14px; background-position: -4867px 0px; } .iti__flag.iti__tg { height: 13px; background-position: -4889px 0px; } .iti__flag.iti__th { height: 14px; background-position: -4911px 0px; } .iti__flag.iti__tj { height: 10px; background-position: -4933px 0px; } .iti__flag.iti__tk { height: 10px; background-position: -4955px 0px; } .iti__flag.iti__tl { height: 10px; background-position: -4977px 0px; } .iti__flag.iti__tm { height: 14px; background-position: -4999px 0px; } .iti__flag.iti__tn { height: 14px; background-position: -5021px 0px; } .iti__flag.iti__to { height: 10px; background-position: -5043px 0px; } .iti__flag.iti__tr { height: 14px; background-position: -5065px 0px; } .iti__flag.iti__tt { height: 12px; background-position: -5087px 0px; } .iti__flag.iti__tv { height: 10px; background-position: -5109px 0px; } .iti__flag.iti__tw { height: 14px; background-position: -5131px 0px; } .iti__flag.iti__tz { height: 14px; background-position: -5153px 0px; } .iti__flag.iti__ua { height: 14px; background-position: -5175px 0px; } .iti__flag.iti__ug { height: 14px; background-position: -5197px 0px; } .iti__flag.iti__um { height: 11px; background-position: -5219px 0px; } .iti__flag.iti__un { height: 14px; background-position: -5241px 0px; } .iti__flag.iti__us { height: 11px; background-position: -5263px 0px; } .iti__flag.iti__uy { height: 14px; background-position: -5285px 0px; } .iti__flag.iti__uz { height: 10px; background-position: -5307px 0px; } .iti__flag.iti__va { height: 15px; background-position: -5329px 0px; } .iti__flag.iti__vc { height: 14px; background-position: -5346px 0px; } .iti__flag.iti__ve { height: 14px; background-position: -5368px 0px; } .iti__flag.iti__vg { height: 10px; background-position: -5390px 0px; } .iti__flag.iti__vi { height: 14px; background-position: -5412px 0px; } .iti__flag.iti__vn { height: 14px; background-position: -5434px 0px; } .iti__flag.iti__vu { height: 12px; background-position: -5456px 0px; } .iti__flag.iti__wf { height: 14px; background-position: -5478px 0px; } .iti__flag.iti__ws { height: 10px; background-position: -5500px 0px; } .iti__flag.iti__xk { height: 15px; background-position: -5522px 0px; } .iti__flag.iti__ye { height: 14px; background-position: -5544px 0px; } .iti__flag.iti__yt { height: 14px; background-position: -5566px 0px; } .iti__flag.iti__za { height: 14px; background-position: -5588px 0px; } .iti__flag.iti__zm { height: 14px; background-position: -5610px 0px; } .iti__flag.iti__zw { height: 10px; background-position: -5632px 0px; } .iti__flag { height: 15px; box-shadow: 0px 0px 1px 0px #888; background-image: url(007b2705c0a8f69dfdf6.png); background-repeat: no-repeat; background-color: #DBDBDB; background-position: 20px 0; } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .iti__flag { background-image: url(9d5328fb490cddd43f66.png); } } .iti__flag.iti__np { background-color: transparent; } #payment .wc_payment_methods.payment_methods.methods+.woopay-save-new-user-container:not(:empty){margin-top:24px}#payment .wc_payment_methods.payment_methods.methods+.woopay-save-new-user-container:not(:empty)+.place-order{margin-top:24px}.woopay-save-new-user-container h3{margin-bottom:0}.woopay-save-new-user-container .components-checkbox-control__input-container{margin-right:8px;height:auto}.woopay-save-new-user-container .components-checkbox-control__input-container .components-checkbox-control__input[type=checkbox]{height:16px;width:16px}.woopay-save-new-user-container .components-checkbox-control__input-container svg{height:16px;width:16px;left:0;top:0}.woopay-save-new-user-container .components-checkbox-control__input-container label{font-size:14px}.woopay-save-new-user-container .save-details-form.form-row{display:flex;flex-direction:column;font-size:1rem;line-height:1.25rem;font-weight:400;gap:16px;transition:max-height .5s ease-in-out;margin:0 !important;padding:0 !important;overflow-y:hidden;max-height:21.875rem}.woopay-save-new-user-container .save-details-form.form-row::before,.woopay-save-new-user-container .save-details-form.form-row::after{display:none}.woopay-save-new-user-container .save-details-form.form-row>div{margin-top:0}.woopay-save-new-user-container .save-details-form.form-row .additional-information{font-size:var(--wp--preset--font-size--small, 14px);line-height:21px;text-align:left}.woopay-save-new-user-container .save-details-form.form-row .tos{font-size:13px}.woopay-save-new-user-container .save-details-form.form-row #validate-error-invalid-woopay-phone-number{font-size:13px;line-height:1;margin-bottom:0;color:#cc1818}.woopay-save-new-user-container .save-details-form.form-row .line{border:.5px solid #dcdcde}.woopay-save-new-user-container .save-details-form.form-row .phone-input{font-size:1rem;background-color:#fff;box-shadow:none;border:1px solid #ddd;border-radius:5px;width:calc(100% - .25rem)}.woopay-save-new-user-container .save-details-form.form-row .phone-input::placeholder{font-weight:400;color:#a7aaad}.woopay-save-new-user-container .save-details-form.form-row .phone-input.has-error{outline-color:#cc1818}.woopay-save-new-user-container .iti{margin-top:16px}.wc-block-components-form .woopay-save-new-user-container .wc-block-components-text-input:only-child{margin-top:0}#phone-number{height:42px;box-shadow:inset 0 1px #d6d6d6;border:none;font-size:15px;line-height:26px}#payment .save-details-form.place-order{margin-top:8px}.iti__flag{background-image:url(007b2705c0a8f69dfdf6.png)}@media(-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.iti__flag{background-image:url(9d5328fb490cddd43f66.png)}}.iti{width:100%}.iti--container{margin-top:0}.iti__selected-flag{background-color:inherit !important;padding:0 6px 0 12px}.iti__selected-flag .iti__flag{transform:scale(1.1)}.iti__selected-flag .iti__selected-dial-code{font-size:16px;color:#2f2f2f}.iti__selected-flag .iti__arrow{border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #2c3338}.iti__selected-flag .iti__arrow--up{border-top:none;border-bottom:5px solid #2c3338}.wp-admin .iti .iti__arrow{border:0;width:10px;height:18px;background:url("data:image/svg+xml;utf8,<svg viewBox=\"4 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z\"></path></svg>")}.wp-admin .iti .iti__arrow--up{transform:scaleY(-1)}.wp-admin .iti .iti__country-list{border:1px solid #949494;border-radius:2px;box-shadow:none}.wp-admin .iti .iti__selected-dial-code,.wp-admin .iti .phone-input.input-text,.wp-admin .iti .iti__country{font-size:inherit;color:#2c3338}@media(max-width: 782px){.wp-admin .iti .iti__selected-dial-code,.wp-admin .iti .phone-input.input-text,.wp-admin .iti .iti__country{font-size:16px}}.wp-admin.iti-mobile .iti--container{top:76px;width:auto} .woopay-save-new-user-container .save-details .wc-block-components-text-input input:-webkit-autofill{padding:1.5em .5em 1.5em .5em}.woopay-save-new-user-container .save-details-header{display:flex;align-items:flex-start;gap:12px;position:relative}.woopay-save-new-user-container .save-details-header>div{flex:1}.woopay-save-new-user-container .save-details-header label{display:block;margin:0 !important;padding:0 0 0 27px;text-indent:-27px}.woopay-save-new-user-container .save-details-header label .save-details-checkbox:not(.wc-block-components-checkbox__input){margin-right:12px}.woopay-save-new-user-container .save-details-header .wc-block-components-checkbox{margin-top:0;flex:1}.woopay-save-new-user-container .save-details-header .wc-block-components-checkbox label{display:flex !important;align-items:flex-start;padding:0;text-indent:0}@media(min-width: 961px){.woopay-save-new-user-container .save-details-header .wc-block-components-checkbox span{margin-right:1.25rem}}.woopay-save-new-user-container h2{margin-bottom:4px} dist/tos.js 0000777 00000021010 15251156605 0006661 0 ustar 00 (()=>{var e={5338:(e,t,o)=>{"use strict";var n=o(5795);t.H=n.createRoot,n.hydrateRoot},5795:e=>{"use strict";e.exports=window.ReactDOM}},t={};function o(n){var r=t[n];if(void 0!==r)return r.exports;var c=t[n]={exports:{}};return e[n](c,c.exports,o),c.exports}o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var t=o.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var n=t.getElementsByTagName("script");if(n.length)for(var r=n.length-1;r>-1&&(!e||!/^http(s?):/.test(e));)e=n[r--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),o.p=window.wcpayAssets.url,(()=>{"use strict";const e=window.React;var t=o.n(e),n=o(5338);const r=window.wp.i18n;function c(e){return e.startsWith("{{/")?{type:"componentClose",value:e.replace(/\W/g,"")}:e.endsWith("/}}")?{type:"componentSelfClosing",value:e.replace(/\W/g,"")}:e.startsWith("{{")?{type:"componentOpen",value:e.replace(/\W/g,"")}:{type:"string",value:e}}function s(t,o){let n,r,c=[];for(let e=0;e<t.length;e++){const s=t[e];if("string"!==s.type){if(void 0===o[s.value])throw new Error(`Invalid interpolation, missing component node: \`${s.value}\``);if("object"!=typeof o[s.value])throw new Error(`Invalid interpolation, component node must be a ReactElement or null: \`${s.value}\``);if("componentClose"===s.type)throw new Error(`Missing opening component token: \`${s.value}\``);if("componentOpen"===s.type){n=o[s.value],r=e;break}c.push(o[s.value])}else c.push(s.value)}if(n){const i=function(e,t){const o=t[e];let n=0;for(let r=e+1;r<t.length;r++){const e=t[r];if(e.value===o.value){if("componentOpen"===e.type){n++;continue}if("componentClose"===e.type){if(0===n)return r;n--}}}throw new Error("Missing closing component token `"+o.value+"`")}(r,t),a=s(t.slice(r+1,i),o),l=(0,e.cloneElement)(n,{},a);if(c.push(l),i<t.length-1){const e=s(t.slice(i+1),o);c=c.concat(e)}}return c=c.filter(Boolean),0===c.length?null:1===c.length?c[0]:(0,e.createElement)(e.Fragment,null,...c)}function i(e){const{mixedString:t,components:o,throwErrors:n}=e;if(!o)return t;if("object"!=typeof o){if(n)throw new Error(`Interpolation Error: unable to process \`${t}\` because components is not an object`);return t}const r=function(e){return e.split(/(\{\{\/?\s*\w+\s*\/?\}\})/g).map(c)}(t);try{return s(r,o)}catch(e){if(n)throw new Error(`Interpolation Error: unable to process \`${t}\` because of error \`${e.message}\``);return t}}const a=window.wp.components,l=window.wp.url,m=(window.lodash,window.moment,window.wp.date,window.wc.number,e=>(0,l.addQueryArgs)("admin.php",e)),p=window.wp.apiFetch;var u=o.n(p);const d=window.wp.domReady;var w=o.n(d),y=function(e,t,o,n){return new(o||(o=Promise))((function(r,c){function s(e){try{a(n.next(e))}catch(e){c(e)}}function i(e){try{a(n.throw(e))}catch(e){c(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof o?t:new o((function(e){e(t)}))).then(s,i)}a((n=n.apply(e,t||[])).next())}))};const v=({accept:e})=>y(void 0,void 0,void 0,(function*(){return u()({path:"/wc/v3/payments/tos",method:"POST",data:{accept:e}})})),f=()=>y(void 0,void 0,void 0,(function*(){const e=wcpay_tos_settings.trackStripeConnected;wcTracks.isEnabled&&e&&(((e,t={})=>{var o;if(window.wcpaySettings){Object.assign(t,{is_test_mode:wcpaySettings.testMode,jetpack_connected:wcpaySettings.isJetpackConnected,wcpay_version:wcpaySettings.version,woo_country_code:wcpaySettings.connect.country,hosting_provider:null===(o=wcpaySettings.trackingInfo)||void 0===o?void 0:o.hosting_provider});for(const e in t)void 0===t[e]&&delete t[e]}w()((()=>{var o,n;(null!==(n=null===(o=null===wc||void 0===wc?void 0:wc.tracks)||void 0===o?void 0:o.recordEvent)&&void 0!==n?n:wcTracks.recordEvent)(e,t)}))})("wcpay_stripe_connected",{is_existing_stripe_account:e.is_existing_stripe_account}),u()({path:"/wc/v3/payments/tos/stripe_track_connected",method:"POST"}))}));var _=function(e,t,o,n){return new(o||(o=Promise))((function(r,c){function s(e){try{a(n.next(e))}catch(e){c(e)}}function i(e){try{a(n.throw(e))}catch(e){c(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof o?t:new o((function(e){e(t)}))).then(s,i)}a((n=n.apply(e,t||[])).next())}))};const g=e=>t().createElement(a.ExternalLink,Object.assign({},e,{href:"https://wordpress.com/tos"})),h=({onAccept:e,onDecline:o,isBusy:n,hasError:c})=>{const s=(0,r.sprintf)((0,r.__)("%s: Terms of Service","woocommerce-payments"),"WooPayments"),l=i({mixedString:(0,r.sprintf)((0,r.__)("To continue using %s, please review and agree to our {{link}}Terms of Service{{/link}}. By clicking “Accept” you agree to the Terms of Service.","woocommerce-payments"),"WooPayments"),components:{link:t().createElement(g,null)}});return t().createElement(a.Modal,{title:s,isDismissible:!1,shouldCloseOnClickOutside:!1,shouldCloseOnEsc:!1,onRequestClose:o,className:"woocommerce-payments__tos-modal"},c&&t().createElement(a.Notice,{status:"error",isDismissible:!1,className:"woocommerce-payments__tos-error"},(0,r.__)("Something went wrong. Please try accepting the Terms of Service again!","woocommerce-payments")),t().createElement("div",{className:"woocommerce-payments__tos-wrapper"},t().createElement("div",{className:"woocommerce-payments__tos-modal-message"},l),t().createElement("div",{className:"woocommerce-payments__tos-footer"},t().createElement(a.Button,{variant:"secondary",onClick:o,disabled:n,__next40pxDefaultSize:!0},(0,r.__)("Decline","woocommerce-payments")),t().createElement(a.Button,{variant:"primary",onClick:e,isBusy:n,__next40pxDefaultSize:!0},(0,r.__)("Accept","woocommerce-payments")))))},E=({onDisable:e,onCancel:o,isBusy:n,hasError:c})=>{const s=(0,r.sprintf)((0,r.__)("Disable %s","woocommerce-payments"),"WooPayments"),l=i({mixedString:(0,r.sprintf)((0,r.__)("By declining our {{link}}Terms of Service{{/link}}, you’ll no longer be able to capture credit card payments using %s. Your previous transaction and payout data will still be available.","woocommerce-payments"),"WooPayments"),components:{link:t().createElement(g,null)}});return t().createElement(a.Modal,{title:s,isDismissible:!1,shouldCloseOnClickOutside:!1,shouldCloseOnEsc:!1,onRequestClose:e,className:"woocommerce-payments__tos-modal"},c&&t().createElement(a.Notice,{status:"error",isDismissible:!1,className:"woocommerce-payments__tos-error"},(0,r.__)("Something went wrong. Please try again!","woocommerce-payments")),t().createElement("div",{className:"woocommerce-payments__tos-wrapper"},t().createElement("div",{className:"woocommerce-payments__tos-modal-message"},l),t().createElement("div",{className:"woocommerce-payments__tos-footer"},t().createElement(a.Button,{variant:"tertiary",onClick:o,isBusy:n,__next40pxDefaultSize:!0},(0,r.__)("Back","woocommerce-payments")),t().createElement(a.Button,{variant:"primary",onClick:e,isBusy:n,__next40pxDefaultSize:!0},(0,r.__)("Disable","woocommerce-payments")))))},b=()=>{const[o,n]=(0,e.useState)(!0),[r,c]=(0,e.useState)(!1),[s,i]=(0,e.useState)(!1),[a,p]=(0,e.useState)(!1),[u,d]=(0,e.useState)(!1),w=()=>n(!1),y=()=>c(!1);return r?t().createElement(E,{onDisable:()=>_(void 0,void 0,void 0,(function*(){try{d(!1),i(!0),yield v({accept:!1}),y(),window.location.href=(0,l.addQueryArgs)(m({page:"wc-settings",tab:"checkout"}),{"tos-disabled":1})}catch(e){d(!0)}finally{i(!1)}})),onCancel:()=>{y(),n(!0)},isBusy:s,hasError:u}):o?t().createElement(h,{onAccept:()=>_(void 0,void 0,void 0,(function*(){try{p(!1),i(!0),yield v({accept:!0}),f(),w()}catch(e){p(!0)}finally{i(!1)}})),onDecline:()=>{w(),c(!0)},isBusy:s,hasError:a}):null},S=window.wp.data;wcpay_tos_settings.tosAgreementDeclined&&window.addEventListener("load",(()=>{const{settingsUrl:e}=wcpay_tos_settings;(e=>{const{createInfoNotice:t}=(0,S.dispatch)("core/notices");t((0,r.sprintf)((0,r.__)("Disabled %s","woocommerce-payments"),"WooPayments"),{actions:[{label:(0,r.__)("Undo","woocommerce-payments"),onClick:async()=>{try{await y(void 0,void 0,void 0,(function*(){return u()({path:"/wc/v3/payments/tos/reactivate",method:"POST"})}))}finally{window.location=e}}}]})})(e)})),wcpay_tos_settings.tosAgreementRequired?function(){const e=document.createElement("div");e.id="wcpay-tos-container",document.body.appendChild(e),(0,n.H)(e).render(t().createElement(b,null))}():f()})()})(); dist/multi-currency-switcher-block.js 0000777 00000242735 15251156605 0013777 0 ustar 00 /*! For license information please see multi-currency-switcher-block.js.LICENSE.txt */ (()=>{var e={1020:(e,t,r)=>{"use strict";var o=r(1609),a=Symbol.for("react.element"),n=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),s=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var o,c={},d=null,u=null;for(o in void 0!==r&&(d=""+r),void 0!==t.key&&(d=""+t.key),void 0!==t.ref&&(u=t.ref),t)n.call(t,o)&&!i.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:a,type:e,key:d,ref:u,props:c,_owner:s.current}}t.jsx=c,t.jsxs=c},4848:(e,t,r)=>{"use strict";e.exports=r(1020)},1609:e=>{"use strict";e.exports=window.React}},t={};function r(o){var a=t[o];if(void 0!==a)return a.exports;var n=t[o]={exports:{}};return e[o](n,n.exports,r),n.exports}r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.g.importScripts&&(e=r.g.location+"");var t=r.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var o=t.getElementsByTagName("script");if(o.length)for(var a=o.length-1;a>-1&&(!e||!/^http(s?):/.test(e));)e=o[a--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),r.p=e})(),r.p=window.wcpayAssets.url,(()=>{"use strict";var e={};r.r(e),r.d(e,{getAllDepositsOverviews:()=>X,getAllDepositsOverviewsError:()=>ee,getDeposit:()=>J,getDepositQueryError:()=>ae,getDeposits:()=>re,getDepositsCount:()=>oe,getDepositsSummary:()=>se,getDepositsSummaryError:()=>ie,getInstantDeposit:()=>ce});var t={};r.r(t),r.d(t,{submitInstantDeposit:()=>Fe,updateAllDepositsOverviews:()=>be,updateDeposit:()=>ve,updateDeposits:()=>Ae,updateDepositsCount:()=>Ne,updateDepositsSummary:()=>Pe,updateErrorForAllDepositsOverviews:()=>Ie,updateErrorForDepositQuery:()=>Ce,updateErrorForDepositsSummary:()=>De,updateInstantDeposit:()=>Me});var o={};r.r(o),r.d(o,{getAllDepositsOverviews:()=>Le,getDeposit:()=>Ue,getDeposits:()=>Ye,getDepositsSummary:()=>ze,getPayoutsCSVRequestURL:()=>je,payoutsDownloadEndpoint:()=>xe});var a={};r.r(a),r.d(a,{getFraudOutcomeTransactions:()=>Je,getFraudOutcomeTransactionsError:()=>Xe,getFraudOutcomeTransactionsSummary:()=>tt,getFraudOutcomeTransactionsSummaryError:()=>rt,getTransactions:()=>Ve,getTransactionsError:()=>Ge,getTransactionsSummary:()=>We,getTransactionsSummaryError:()=>Ze});var n={};r.r(n),r.d(n,{updateErrorForFraudOutcomeTransactions:()=>ct,updateErrorForFraudOutcomeTransactionsSummary:()=>ut,updateErrorForTransactions:()=>at,updateErrorForTransactionsSummary:()=>st,updateFraudOutcomeTransactions:()=>it,updateFraudOutcomeTransactionsSummary:()=>dt,updateTransactions:()=>ot,updateTransactionsSummary:()=>nt});var s={};r.r(s),r.d(s,{formatQueryFilters:()=>_t,getFraudOutcomeTransactions:()=>ht,getFraudOutcomeTransactionsExport:()=>Et,getFraudOutcomeTransactionsSummary:()=>St,getTransactions:()=>pt,getTransactionsCSVRequestURL:()=>lt,getTransactionsSummary:()=>yt,transactionsDownloadEndpoint:()=>mt});var i={};r.r(i),r.d(i,{getCharge:()=>Tt,getChargeError:()=>Rt,getChargeFromOrder:()=>wt,getChargeFromOrderError:()=>ft});var c={};r.r(c),r.d(c,{updateCharge:()=>Ot,updateErrorForCharge:()=>vt});var d={};r.r(d),r.d(d,{getCharge:()=>bt,getChargeFromOrder:()=>It});var u={};r.r(u),r.d(u,{getTimeline:()=>Nt,getTimelineError:()=>Ct});var _={};r.r(_),r.d(_,{updateErrorForTimeline:()=>Dt,updateTimeline:()=>Pt});var p={};r.r(p),r.d(p,{getTimeline:()=>Mt});var m={};r.r(m),r.d(m,{getCachedDispute:()=>jt,getDispute:()=>kt,getDisputeError:()=>xt,getDisputes:()=>Yt,getDisputesSummary:()=>zt});var l={};r.r(l),r.d(l,{refundCharge:()=>Vt,updateErrorForPaymentIntent:()=>Ht,updatePaymentIntent:()=>$t});var y={};r.r(y),r.d(y,{getPaymentIntent:()=>Gt});var h={};r.r(h),r.d(h,{acceptDispute:()=>Jt,updateDispute:()=>Qt,updateDisputes:()=>Zt,updateDisputesSummary:()=>Kt,updateErrorForDispute:()=>Wt});var S={};r.r(S),r.d(S,{disputesDownloadEndpoint:()=>nr,getDispute:()=>ir,getDisputes:()=>cr,getDisputesCSVRequestURL:()=>sr,getDisputesSummary:()=>dr});var E={};r.r(E),r.d(E,{getAccountBusinessSupportEmail:()=>br,getAccountBusinessSupportPhone:()=>Ir,getAccountCommunicationsEmail:()=>mo,getAccountDomesticCurrency:()=>Ar,getAccountStatementDescriptor:()=>fr,getAccountStatementDescriptorKana:()=>vr,getAccountStatementDescriptorKanji:()=>Or,getAdvancedFraudProtectionSettings:()=>so,getAvailablePaymentMethodIds:()=>gr,getCompletedWaitingPeriod:()=>Mr,getCurrentProtectionLevel:()=>no,getDepositDelayDays:()=>Dr,getDepositRestrictions:()=>Ur,getDepositScheduleInterval:()=>Nr,getDepositScheduleMonthlyAnchor:()=>Pr,getDepositScheduleWeeklyAnchor:()=>Cr,getDepositStatus:()=>Fr,getDuplicatedPaymentMethodIds:()=>hr,getEnabledPaymentMethodIds:()=>Er,getExpressCheckoutCartMethods:()=>yo,getExpressCheckoutCheckoutMethods:()=>ho,getExpressCheckoutProductMethods:()=>lo,getIsAmazonPayEnabled:()=>to,getIsAppleGooglePayInPaymentMethodsOptionsEnabled:()=>zr,getIsCardPresentEligible:()=>Zr,getIsDebugLogEnabled:()=>qr,getIsDevModeEnabled:()=>jr,getIsManualCaptureEnabled:()=>Lr,getIsMultiCurrencyEnabled:()=>Br,getIsPaymentRequestEnabled:()=>Yr,getIsSavedCardsEnabled:()=>Qr,getIsStripeBillingEnabled:()=>co,getIsStripeBillingMigrationInProgress:()=>uo,getIsSubscriptionsPluginActive:()=>Xr,getIsTestModeEnabled:()=>kr,getIsTestModeOnboarding:()=>xr,getIsWCPayEnabled:()=>Sr,getIsWCPaySubscriptionsEligible:()=>Jr,getIsWCPaySubscriptionsEnabled:()=>Kr,getIsWooPayEnabled:()=>eo,getIsWooPayGlobalThemeSupportEnabled:()=>ro,getPaymentMethodStatuses:()=>Tr,getPaymentRequestButtonBorderRadius:()=>Gr,getPaymentRequestButtonSize:()=>Hr,getPaymentRequestButtonTheme:()=>Vr,getPaymentRequestButtonType:()=>$r,getSavingError:()=>Wr,getSettings:()=>yr,getShowWooPayIncompatibilityNotice:()=>io,getStripeBillingMigratedCount:()=>po,getStripeBillingSubscriptionCount:()=>_o,getWooPayCustomMessage:()=>oo,getWooPayStoreLogo:()=>ao,isDirty:()=>wr,isSavingSettings:()=>Rr});var g={};r.r(g),r.d(g,{saveOption:()=>ia,saveSettings:()=>Go,submitStripeBillingSubscriptionMigration:()=>sa,updateAccountBusinessSupportEmail:()=>qo,updateAccountBusinessSupportPhone:()=>Bo,updateAccountCommunicationsEmail:()=>ra,updateAccountStatementDescriptor:()=>jo,updateAccountStatementDescriptorKana:()=>zo,updateAccountStatementDescriptorKanji:()=>Yo,updateAdvancedFraudProtectionSettings:()=>ea,updateDepositScheduleInterval:()=>$o,updateDepositScheduleMonthlyAnchor:()=>Vo,updateDepositScheduleWeeklyAnchor:()=>Ho,updateEnabledPaymentMethodIds:()=>Co,updateExpressCheckoutCartMethods:()=>aa,updateExpressCheckoutCheckoutMethods:()=>na,updateExpressCheckoutProductMethods:()=>oa,updateIsAmazonPayEnabled:()=>Wo,updateIsAppleGooglePayInPaymentMethodsOptionsEnabled:()=>No,updateIsCardPresentEligible:()=>Ro,updateIsDebugLogEnabled:()=>Lo,updateIsManualCaptureEnabled:()=>Fo,updateIsMultiCurrencyEnabled:()=>ko,updateIsPaymentRequestEnabled:()=>Ao,updateIsSavedCardsEnabled:()=>To,updateIsSavingSettings:()=>Po,updateIsStripeBillingEnabled:()=>ta,updateIsTestModeEnabled:()=>Uo,updateIsWCPayEnabled:()=>Io,updateIsWCPaySubscriptionsEnabled:()=>xo,updateIsWooPayEnabled:()=>Qo,updateIsWooPayGlobalThemeSupportEnabled:()=>Zo,updatePaymentRequestButtonBorderRadius:()=>vo,updatePaymentRequestButtonSize:()=>fo,updatePaymentRequestButtonTheme:()=>Oo,updatePaymentRequestButtonType:()=>wo,updateProtectionLevel:()=>Xo,updateSelectedPaymentMethod:()=>Do,updateSettings:()=>bo,updateUnselectedPaymentMethod:()=>Mo,updateWooPayCustomMessage:()=>Ko,updateWooPayStoreLogo:()=>Jo});var T={};r.r(T),r.d(T,{getSettings:()=>ca});var R={};r.r(R),r.d(R,{getCardReaderStats:()=>_a,getCardReaderStatsError:()=>pa,getCardReaders:()=>ma});var w={};r.r(w),r.d(w,{updateCardReaderStats:()=>la,updateCardReaders:()=>ya,updateErrorForCardReaderStats:()=>ha,updateErrorForCardReaders:()=>Sa});var f={};r.r(f),r.d(f,{getCardReaderStats:()=>Ea,getCardReaders:()=>ga});var O={};r.r(O),r.d(O,{getActiveLoanSummary:()=>Oa,getActiveLoanSummaryError:()=>va,getLoans:()=>ba,getLoansError:()=>Ia});var v={};r.r(v),r.d(v,{updateActiveLoanSummary:()=>Aa,updateErrorForActiveLoanSummary:()=>Na,updateErrorForLoans:()=>Pa,updateLoans:()=>Ca});var b={};r.r(b),r.d(b,{getActiveLoanSummary:()=>Da,getLoans:()=>Ma});var I={};r.r(I),r.d(I,{getDocuments:()=>xa,getDocumentsError:()=>ja,getDocumentsSummary:()=>za,getDocumentsSummaryError:()=>qa});var A={};r.r(A),r.d(A,{updateDocuments:()=>Ba,updateDocumentsSummary:()=>Ha,updateErrorForDocuments:()=>$a,updateErrorForDocumentsSummary:()=>Va});var N={};r.r(N),r.d(N,{formatQueryFilters:()=>Ga,getDocuments:()=>Qa,getDocumentsSummary:()=>Wa});var C={};r.r(C),r.d(C,{getPaymentIntent:()=>Ka,getPaymentIntentError:()=>Ja});var P={};r.r(P),r.d(P,{getAuthorization:()=>an,getAuthorizations:()=>on,getAuthorizationsError:()=>nn,getAuthorizationsSummary:()=>cn,getAuthorizationsSummaryError:()=>dn,getIsRequesting:()=>un});var D={};r.r(D),r.d(D,{setIsRequestingAuthorization:()=>hn,submitCancelAuthorization:()=>En,submitCaptureAuthorization:()=>Sn,updateAuthorization:()=>ln,updateAuthorizations:()=>pn,updateAuthorizationsSummary:()=>yn,updateErrorForAuthorizations:()=>mn,updateErrorForAuthorizationsSummary:()=>gn});var M={};r.r(M),r.d(M,{getAuthorization:()=>Rn,getAuthorizations:()=>Tn,getAuthorizationsSummary:()=>wn});var F={};r.r(F),r.d(F,{getFile:()=>bn,getFileError:()=>In});var U={};r.r(U),r.d(U,{updateErrorForFiles:()=>Nn,updateFiles:()=>An});var L={};r.r(L),r.d(L,{getFile:()=>Cn});var k={};r.r(k),r.d(k,{getPmPromotions:()=>Mn,getPmPromotionsError:()=>Fn});var x={};r.r(x),r.d(x,{activatePmPromotion:()=>xn,dismissPmPromotion:()=>jn,updateErrorForPmPromotions:()=>kn,updatePmPromotions:()=>Ln});var j={};r.r(j),r.d(j,{getPmPromotions:()=>zn});var Y={};r.r(Y),r.d(Y,{getAvailableCurrencies:()=>Qn,getCurrencies:()=>Gn,getCurrencySettings:()=>Kn,getDefaultCurrency:()=>Zn,getEnabledCurrencies:()=>Wn,getStoreSettings:()=>Jn});var z={};r.r(z),r.d(z,{saveOption:()=>cs,submitCurrencySettings:()=>ss,submitEnabledCurrenciesUpdate:()=>ns,submitStoreSettingsUpdate:()=>is,updateAvailableCurrencies:()=>es,updateCurrencies:()=>Xn,updateCurrencySettings:()=>os,updateDefaultCurrency:()=>rs,updateEnabledCurrencies:()=>ts,updateStoreSettings:()=>as});var q={};r.r(q),r.d(q,{getCurrencies:()=>ds,getCurrencySettings:()=>us,getStoreSettings:()=>_s});const B=window.wp.data,$=window.wp.dataControls,H="/wc/v3/payments",V="wc/payments",G=window.lodash,Q={SET_DEPOSIT:"SET_DEPOSIT",SET_DEPOSITS:"SET_DEPOSITS",SET_DEPOSITS_COUNT:"SET_DEPOSITS_COUNT",SET_ERROR_FOR_DEPOSIT_QUERY:"SET_ERROR_FOR_DEPOSIT_QUERY",SET_DEPOSITS_SUMMARY:"SET_DEPOSITS_SUMMARY",SET_ERROR_FOR_DEPOSITS_SUMMARY:"SET_ERROR_FOR_DEPOSITS_SUMMARY",SET_INSTANT_DEPOSIT:"SET_INSTANT_DEPOSIT",SET_ALL_DEPOSITS_OVERVIEWS:"SET_ALL_DEPOSITS_OVERVIEWS",SET_ERROR_FOR_ALL_DEPOSITS_OVERVIEWS:"SET_ERROR_FOR_ALL_DEPOSITS_OVERVIEWS"},W=e=>JSON.stringify(e,Object.keys(e).sort()),Z={byId:{},queries:{},count:0,instant:{}},K=e=>e&&e.deposits||{},J=(e,t)=>(K(e).byId||{})[t],X=e=>{const t=K(e).overviews||{};if(!t.data)return{account:null,currencies:[]};const{deposit:r,balance:o,account:a}=t.data,n={lastPaid:r.last_paid,pending:o.pending,available:o.available,instant:o.instant},s={};for(const[e,t]of Object.entries(n))t?.forEach((t=>{const{currency:r}=t;s[r]||(s[r]={currency:r,lastPaid:void 0,pending:void 0,available:void 0,instant:void 0}),s[r][e]=t}));const i=Object.values(s);return{account:a,currencies:[...i.filter((e=>a.default_currency===e.currency)),...i.filter((e=>a.default_currency!==e.currency))]}},ee=e=>(K(e).overviews||{}).error,te=(e,t)=>{const r=W(t);return(K(e).queries||{})[r]||{}},re=(e,t)=>(te(e,t).data||[]).map(J.bind(void 0,e)),oe=e=>K(e).count,ae=(e,t)=>te(e,t).error||{},ne=(e,t)=>{const r=W(t);return(K(e).summary||{})[r]||{}},se=(e,t)=>ne(e,t).data||{},ie=(e,t)=>ne(e,t).error||{},ce=e=>(K(e).instant||{}).data,de=window.wp.i18n,ue=window.wp.domReady;var _e=r.n(ue);const pe=(e,t={})=>{var r;if(window.wcpaySettings){Object.assign(t,{is_test_mode:wcpaySettings.testMode,jetpack_connected:wcpaySettings.isJetpackConnected,wcpay_version:wcpaySettings.version,woo_country_code:wcpaySettings.connect.country,hosting_provider:null===(r=wcpaySettings.trackingInfo)||void 0===r?void 0:r.hosting_provider});for(const e in t)void 0===t[e]&&delete t[e]}_e()((()=>{var r,o;(null!==(o=null===(r=null===wc||void 0===wc?void 0:wc.tracks)||void 0===r?void 0:r.recordEvent)&&void 0!==o?o:wcTracks.recordEvent)(e,t)}))};var me=r(1609);(0,me.createContext)(window.wcpaySettings),(0,me.createContext)({isActive:!1,setActive:()=>null,isCompleted:!1,setCompleted:()=>null,taskId:""});const le=window.wc.currency;var ye=r.n(le);(0,de.__)("Australian dollar","woocommerce-payments"),(0,de.__)("Canadian dollar","woocommerce-payments"),(0,de.__)("Swiss franc","woocommerce-payments"),(0,de.__)("Danish krone","woocommerce-payments"),(0,de.__)("Euro","woocommerce-payments"),(0,de.__)("Pound sterling","woocommerce-payments"),(0,de.__)("Norwegian krone","woocommerce-payments"),(0,de.__)("New Zealand dollar","woocommerce-payments"),(0,de.__)("Swedish krona","woocommerce-payments"),(0,de.__)("United States (US) dollar","woocommerce-payments");const he=(e,t="USD",r=null,o=!1)=>{const a=(e=>wcpaySettings.zeroDecimalCurrencies.includes(e.toLowerCase()))(t);a||(e/=100);const n=e<0,s=n?-1*e:e,i=n?"-":"",c=o?(e=>{const t=e.toUpperCase(),{currencyData:r,connect:{country:o="US"}}=wcpaySettings;if(r[o]?.code===t)return ye()(r[o]);const a=(0,G.find)(r,{code:t});if(a){const{defaultLocale:e={}}=a;return e.hasOwnProperty("decimalSeparator")&&e.hasOwnProperty("thousandSeparator")&&e.hasOwnProperty("symbolPosition")&&(a.decimalSeparator=e.decimalSeparator,a.thousandSeparator=e.thousandSeparator,a.symbolPosition=e.symbolPosition),ye()(a)}return null})(t):((e,t=null)=>{const{currencyData:r,connect:{country:o="US"}}=wcpaySettings,a=(0,G.find)(r,{code:e.toUpperCase()});if(a){if(null!==t&&t.toUpperCase()!==e.toUpperCase()||r[o]){const e=t?(0,G.find)(r,{code:t.toUpperCase()}):r[o];e&&(a.decimalSeparator=e.decimalSeparator,a.thousandSeparator=e.thousandSeparator,a.symbolPosition=e.symbolPosition)}return ye()(a)}return null})(t,r);if(null===c)return i+Se(s,t,a);try{return i+("function"==typeof c.formatAmount?Ee(c.formatAmount(s)):Ee(c.formatCurrency(s)))}catch(e){return i+Ee(Se(s,t,a))}};function Se(e,t,r){try{return e.toLocaleString(void 0,{style:"currency",currency:t,currencyDisplay:"narrowSymbol",dummy:r})}catch(o){return(0,de.sprintf)(r?"%s %i":"%s %.2f",t.toUpperCase(),e)}}function Ee(e){return(new DOMParser).parseFromString(e,"text/html").documentElement.textContent}const ge=window.wp.url,Te=window.moment;var Re=r.n(Te);const we=window.wp.date,fe=(window.wc.number,e=>(0,ge.addQueryArgs)("admin.php",e)),Oe=(e,t=!1)=>{const r=t?Re()(e).endOf("day").utc().toISOString():Re()(e).startOf("day").utc().toISOString();return e&&(0,we.dateI18n)("Y-m-d H:i:s",r,!0)};function ve(e){return{type:Q.SET_DEPOSIT,data:e}}function be(e){return{type:Q.SET_ALL_DEPOSITS_OVERVIEWS,data:e}}function Ie(e,t){return{type:Q.SET_ERROR_FOR_ALL_DEPOSITS_OVERVIEWS,error:t}}function Ae(e,t){return{type:Q.SET_DEPOSITS,query:e,data:t}}function Ne(e){return{type:Q.SET_DEPOSITS_COUNT,data:e}}function Ce(e,t,r){return{type:Q.SET_ERROR_FOR_DEPOSIT_QUERY,query:e,data:t,error:r}}function Pe(e,t){return{type:Q.SET_DEPOSITS_SUMMARY,query:e,data:t}}function De(e,t,r){return{type:Q.SET_ERROR_FOR_DEPOSITS_SUMMARY,query:e,data:t,error:r}}function Me(e){return{type:Q.SET_INSTANT_DEPOSIT,data:e}}function*Fe(e){try{yield(0,B.dispatch)(V).startResolution("getInstantDeposit",[e]);const t=yield(0,$.apiFetch)({path:"/wc/v3/payments/deposits",method:"POST",data:{type:"instant",currency:e}});yield Me(t),yield(0,B.dispatch)(V).invalidateResolutionForStoreSelector("getDeposits"),yield(0,B.dispatch)(V).invalidateResolutionForStoreSelector("getAllDepositsOverviews"),yield(0,B.dispatch)("core/notices").createSuccessNotice((0,de.sprintf)((0,de.__)("Instant payout for %s in transit.","woocommerce-payments"),he(t.amount)),{actions:[{label:(0,de.__)("View details","woocommerce-payments"),url:fe({page:"wc-admin",path:"/payments/payouts/details",id:t.id})}]})}catch{yield(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error creating instant payout.","woocommerce-payments"))}finally{yield(0,B.dispatch)(V).finishResolution("getInstantDeposit",[e])}}function*Ue(e){if(/\W/.test(e))return;const t=(0,ge.addQueryArgs)(`${H}/deposits/${e}`);try{const e=yield(0,$.apiFetch)({path:t});yield ve(e)}catch(e){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving payout.","woocommerce-payments"))}}function*Le(){const e=(0,ge.addQueryArgs)(`${H}/deposits/overview-all`);try{const t=yield(0,$.apiFetch)({path:e});yield be(t)}catch(e){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving all payouts' overviews.","woocommerce-payments")),yield Ie()}}const ke=e=>({user_email:e.userEmail,match:e.match,store_currency_is:e.storeCurrencyIs,date_before:Oe(e.dateBefore,!0),date_after:Oe(e.dateAfter),date_between:e.dateBetween&&[Oe(e.dateBetween[0]),Oe(e.dateBetween[1],!0)],status_is:e.statusIs,status_is_not:e.statusIsNot,locale:e.locale}),xe=`${H}/deposits/download`;function je(e){return(0,ge.addQueryArgs)(xe,ke(e))}function*Ye(e){const t=(0,ge.addQueryArgs)(`${H}/deposits`,{page:e.paged,pagesize:e.perPage,sort:e.orderby,direction:e.order,...ke(e)});try{const r=yield(0,$.apiFetch)({path:t})||{};yield Ae(e,r.data),yield Ne(r.total_count);for(const e in r.data)yield B.controls.dispatch(V,"finishResolution","getDeposit",[r.data[e].id])}catch(t){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving payouts.","woocommerce-payments")),yield Ce(e,null,t)}}function*ze(e){const t=(0,ge.addQueryArgs)(`${H}/deposits/summary`,ke(e));try{const r=yield(0,$.apiFetch)({path:t});yield Pe(e,r)}catch(t){yield De(e,null,t)}}const qe={SET_TRANSACTIONS:"SET_TRANSACTIONS",SET_ERROR_FOR_TRANSACTIONS:"SET_ERROR_FOR_TRANSACTIONS",SET_TRANSACTIONS_SUMMARY:"SET_TRANSACTIONS_SUMMARY",SET_ERROR_FOR_TRANSACTIONS_SUMMARY:"SET_ERROR_FOR_TRANSACTIONS_SUMMARY",SET_FRAUD_OUTCOME_TRANSACTIONS:"SET_FRAUD_OUTCOME_TRANSACTIONS",SET_ERROR_FOR_FRAUD_OUTCOME_TRANSACTIONS:"SET_ERROR_FOR_FRAUD_OUTCOME_TRANSACTIONS",SET_FRAUD_OUTCOME_TRANSACTIONS_SUMMARY:"SET_FRAUD_OUTCOME_TRANSACTIONS_SUMMARY",SET_ERROR_FOR_FRAUD_OUTCOME_TRANSACTIONS_SUMMARY:"SET_ERROR_FOR_FRAUD_OUTCOME_TRANSACTIONS_SUMMARY"},Be={summary:{},fraudProtection:{review:{summary:{}},block:{summary:{}}}},$e=e=>e&&e.transactions||{},He=(e,t)=>{const r=W(t);return $e(e)[r]||{}},Ve=(e,t)=>He(e,t).data||[],Ge=(e,t)=>He(e,t).error||{},Qe=(e,t)=>{const r=W(t);return $e(e).summary[r]||{}},We=(e,t)=>Qe(e,t).data||{},Ze=(e,t)=>Qe(e,t).error||{},Ke=(e,t,r)=>{const o=W(r);return $e(e).fraudProtection?.[t]?.[o]||{}},Je=(e,t,r)=>Ke(e,t,r).data||[],Xe=(e,t,r)=>Ke(e,t,r).error||{},et=(e,t,r)=>{const o=W(r);return $e(e).fraudProtection?.[t]?.summary[o]||{}},tt=(e,t,r)=>et(e,t,r).data||{},rt=(e,t,r)=>et(e,t,r).error||{};function ot(e,t){return{type:qe.SET_TRANSACTIONS,query:e,data:t}}function at(e,t,r){return{type:qe.SET_ERROR_FOR_TRANSACTIONS,query:e,data:t,error:r}}function nt(e,t){return{type:qe.SET_TRANSACTIONS_SUMMARY,query:e,data:t}}function st(e,t,r){return{type:qe.SET_ERROR_FOR_TRANSACTIONS_SUMMARY,query:e,data:t,error:r}}function it(e,t,r){return{type:qe.SET_FRAUD_OUTCOME_TRANSACTIONS,status:e,query:t,data:r}}function ct(e,t,r){return{type:qe.SET_ERROR_FOR_FRAUD_OUTCOME_TRANSACTIONS,status:e,query:t,data:null,error:r}}function dt(e,t,r){return{type:qe.SET_FRAUD_OUTCOME_TRANSACTIONS_SUMMARY,status:e,query:t,data:r}}function ut(e,t,r){return{type:qe.SET_ERROR_FOR_FRAUD_OUTCOME_TRANSACTIONS_SUMMARY,status:e,query:t,data:null,error:r}}const _t=e=>({user_email:e.userEmail,match:e.match,date_before:Oe(e.dateBefore,!0),date_after:Oe(e.dateAfter),date_between:e.dateBetween&&[Oe(e.dateBetween[0]),Oe(e.dateBetween[1],!0)],type_is:e.typeIs,type_is_not:e.typeIsNot,type_is_in:e.typeIsIn,source_device_is:e.sourceDeviceIs,source_device_is_not:e.sourceDeviceIsNot,channel_is:e.channelIs,channel_is_not:e.channelIsNot,customer_country_is:e.customerCountryIs,customer_country_is_not:e.customerCountryIsNot,risk_level_is:e.riskLevelIs,risk_level_is_not:e.riskLevelIsNot,store_currency_is:e.storeCurrencyIs,loan_id_is:e.loanIdIs,deposit_id:e.depositId,customer_currency_is:e.customerCurrencyIs,customer_currency_is_not:e.customerCurrencyIsNot,source_is:e.sourceIs,source_is_not:e.sourceIsNot,search:e.search,user_timezone:Re()(new Date).format("Z"),locale:e.locale});function*pt(e){const t=(0,ge.addQueryArgs)(`${H}/transactions`,{page:e.paged,pagesize:e.perPage,sort:e.orderby,direction:e.order,..._t(e)});try{const r=yield(0,$.apiFetch)({path:t});yield ot(e,r.data||[])}catch(t){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving transactions.","woocommerce-payments")),yield at(e,null,t)}}const mt=`${H}/transactions/download`;function lt(e){return(0,ge.addQueryArgs)(mt,_t(e))}function*yt(e){const t=(0,ge.addQueryArgs)(`${H}/transactions/summary`,_t(e));try{const r=yield(0,$.apiFetch)({path:t});yield nt(e,r)}catch(t){yield st(e,null,t)}}function*ht(e,t){const r=(0,ge.addQueryArgs)(`${H}/transactions/fraud-outcomes`,{status:e,page:t.paged,sort:t.orderby,pagesize:t.perPage,direction:t.order,additional_status:t.additionalStatus,..._t(t)});try{const o=yield(0,$.apiFetch)({path:r});yield it(e,t,o.data||[])}catch(r){if("wcpay_fraud_outcome_not_found"===r.code)return void(yield it(e,t,[]));yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving transactions.","woocommerce-payments")),yield ct(e,t,r)}}function*St(e,t){const r=(0,ge.addQueryArgs)(`${H}/transactions/fraud-outcomes/summary`,{status:e,additional_status:t.additionalStatus}),o={count:0,total:0};try{const a=yield(0,$.apiFetch)({path:r});yield dt(e,t,a||o)}catch(r){if("wcpay_fraud_outcome_not_found"===r.code)return void(yield dt(e,t,o));yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving on review transactions.","woocommerce-payments")),yield ut(e,t,r)}}function Et(e,t){return(0,ge.addQueryArgs)(`${H}/transactions/fraud-outcomes/download`,{status:e,sort:t.orderby,direction:t.order,additional_status:t.additionalStatus,..._t(t)})}const gt={SET_CHARGE:"SET_CHARGE",SET_ERROR_FOR_CHARGE:"SET_ERROR_FOR_CHARGE"},Tt=(e,t)=>e.charges[t]&&e.charges[t].data?e.charges[t].data:{},Rt=(e,t)=>e.charges[t]&&e.charges[t].error?e.charges[t].error:{},wt=(e,t)=>Tt(e,t),ft=(e,t)=>Rt(e,t);function Ot(e,t){return{type:gt.SET_CHARGE,id:e,data:t}}function vt(e,t,r){return{type:gt.SET_ERROR_FOR_CHARGE,id:e,data:t,error:r}}function*bt(e){try{const t=yield(0,$.apiFetch)({path:`${H}/charges/${e}`});yield Ot(e,t)}catch(t){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving transaction.","woocommerce-payments")),yield vt(e,null,t)}}function*It(e){try{const t=yield(0,$.apiFetch)({path:`${H}/charges/order/${e}`});yield Ot(e,t)}catch(t){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving transaction.","woocommerce-payments")),yield vt(e,null,t)}}const At={SET_TIMELINE:"SET_TIMELINE",SET_ERROR_FOR_TIMELINE:"SET_ERROR_FOR_TIMELINE"},Nt=(e,t)=>e.timeline&&e.timeline[t]&&e.timeline[t].data?e.timeline[t].data:{},Ct=(e,t)=>e.timeline&&e.timeline[t]&&e.timeline[t].error?e.timeline[t].error:{};function Pt(e,t){return{id:e,type:At.SET_TIMELINE,data:t}}function Dt(e,t){return{id:e,type:At.SET_ERROR_FOR_TIMELINE,error:t}}function*Mt(e){try{const t=yield(0,$.apiFetch)({path:`${H}/timeline/${e}`});yield Pt(e,t.data)}catch(t){yield Dt(e,t)}}const Ft={SET_DISPUTE:"SET_DISPUTE",SET_ERROR_FOR_DISPUTE:"SET_ERROR_FOR_DISPUTE",SET_DISPUTES:"SET_DISPUTES",SET_DISPUTES_SUMMARY:"SET_DISPUTES_SUMMARY"},Ut={byId:{},queries:{},summary:{},cached:{}},Lt=e=>e&&e.disputes||{},kt=(e,t)=>(Lt(e).byId||{})[t],xt=(e,t)=>{const r=Lt(e).byId||{};return r[t]?.error},jt=(e,t)=>(Lt(e).cached||{})[t],Yt=(e,t)=>{const r=((e,t)=>{const r=W(t);return(Lt(e).queries||{})[r]||{}})(e,t).data||[];return r.map(jt.bind(void 0,e))},zt=(e,t)=>((e,t)=>{const r=W(t);return(Lt(e).summary||{})[r]||{}})(e,t).data||{};var qt;!function(e){e.SET_PAYMENT_INTENT="SET_PAYMENT_INTENT",e.SET_ERROR_FOR_PAYMENT_INTENT="SET_ERROR_FOR_PAYMENT_INTENT"}(qt||(qt={}));const Bt=qt;function $t(e,t){return{type:Bt.SET_PAYMENT_INTENT,id:e,data:t}}function Ht(e,t){return{type:Bt.SET_ERROR_FOR_PAYMENT_INTENT,id:e,error:t}}function*Vt(e,t){var r;const o=e.payment_intent;try{yield(0,$.apiFetch)({path:"/wc/v3/payments/refund/",method:"post",data:{charge_id:e.id,amount:e.amount,reason:t,order_id:null===(r=null==e?void 0:e.order)||void 0===r?void 0:r.id}}),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getTimeline"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getPaymentIntent"),yield B.controls.dispatch("core/notices","createSuccessNotice",(0,de.sprintf)((0,de.__)("Refunded payment #%s.","woocommerce-payments"),o))}catch(e){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.sprintf)((0,de.__)("There has been an error refunding the payment #%s. Please try again later.","woocommerce-payments"),o))}}function*Gt(e){try{const t=yield(0,$.apiFetch)({path:`${H}/payment_intents/${e}`});yield $t(e,t)}catch(t){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving transaction.","woocommerce-payments")),yield Ht(e,t)}}function Qt(e){return{type:Ft.SET_DISPUTE,data:e}}function Wt(e,t,r){return{type:Ft.SET_ERROR_FOR_DISPUTE,id:e,data:t,error:r}}function Zt(e,t){return{type:Ft.SET_DISPUTES,query:e,data:t}}function Kt(e,t){return{type:Ft.SET_DISPUTES_SUMMARY,query:e,data:t}}function*Jt(e){const{id:t,payment_intent:r}=e;try{yield B.controls.dispatch(V,"startResolution","getDispute",[t]);const e=yield(0,$.apiFetch)({path:`${H}/disputes/${t}/close`,method:"post"});yield Qt(e),yield Gt(r),yield B.controls.dispatch(V,"finishResolution","getDispute",[t]);const o=e.order?(0,de.sprintf)((0,de.__)("You have accepted the dispute for order #%s.","woocommerce-payments"),e.order.number):(0,de.__)("You have accepted the dispute.","woocommerce-payments");yield B.controls.dispatch("core/notices","createSuccessNotice",o)}catch(e){const r=(0,de.__)("There has been an error accepting the dispute. Please try again later.","woocommerce-payments");yield B.controls.dispatch("core/notices","createErrorNotice",r),yield B.controls.dispatch(V,"finishResolution","getDispute",[t])}}const Xt=window.wc.wcSettings,er=((0,de.__)("Bank cannot process","woocommerce-payments"),(0,de.__)("Check returned","woocommerce-payments"),(0,de.__)("Credit not processed","woocommerce-payments"),(0,de.__)("If you believe the dispute is invalid, you can challenge it by submitting the appropriate evidence using the response forms on the next screen.","woocommerce-payments"),(0,de.__)("If your customer was not refunded appropriately, you will need to accept the dispute, or resolve the issue with your customer. The credit card networks place liability for accepting disputed payments with you, the business.","woocommerce-payments"),(0,de.__)("The customer claims that the purchased product was returned or the transaction was otherwise canceled, but you have not yet provided a refund or credit.","woocommerce-payments"),(0,de.__)("Demonstrate that you have refunded your customer through other means or that your customer is not entitled to a refund. You cannot issue a refund while a payment is being disputed. If you believe that your customer was entitled a refund that you did not provide, you can accept the dispute.","woocommerce-payments"),(0,de.__)("You should first get in touch with your customer. If you understand what their complaint is, there is a chance for you to explain the misunderstanding or to make it right. If you’re able to resolve the issue with your customer, you can ask that they withdraw the dispute.","woocommerce-payments"),(0,de.__)("If the cardholder agrees to withdraw the dispute, you should still submit evidence for the dispute using the forms on the next screen. In addition to the following evidence, your submission should include correspondence with the cardholder saying they would withdraw the dispute and a written statement from their card issuer confirming that the dispute has been withdrawn.","woocommerce-payments"),(0,de.__)("The cardholder claims a credit was not processed.","woocommerce-payments"),(0,de.__)("Customer initiated","woocommerce-payments"),(0,de.__)("Debit not authorized","woocommerce-payments"),(0,de.__)("Duplicate","woocommerce-payments"),(0,de.__)("If you believe the dispute is invalid, you can challenge it by submitting the appropriate evidence using the response forms on the next screen.","woocommerce-payments"),(0,de.__)("If there were duplicate payments, you should accept the dispute. You cannot issue a refund while a payment is being disputed. The credit card networks place liability for accepting disputed payments with you, the business.","woocommerce-payments"),(0,de.__)("The customer claims they were charged multiple times for the same product or service.","woocommerce-payments"),(0,de.__)("Demonstrate that each payment was for a separate product or service.","woocommerce-payments"),(0,de.__)("Determine if your customer was incorrectly charged multiple times.","woocommerce-payments"),(0,de.__)("If they were not, collect any and all information documenting that each payment was made separately, such as copies of receipts. If the receipts don’t include the items purchased, be sure to include an itemized list. Each receipt should clearly indicate that the payments are for separate purchases of items or services. If you’ve been able to get in touch with the customer you should be sure to address any concerns they had in your evidence.","woocommerce-payments"),(0,de.__)("If there have been two or more separate payments, you should get in touch with your customer. If you understand what their complaint is, there is a chance for you to explain the misunderstanding or to make it right. If you’re able to resolve the issue with your customer, you can ask that they withdraw the dispute.","woocommerce-payments"),(0,de.__)("Even if your customer agrees to withdraw the dispute, you must still submit appropriate evidence using the forms on the next screen. Simply saying that your customer is going to withdraw the dispute is not sufficient evidence.","woocommerce-payments"),(0,de.__)("The cardholder claims this is a duplicate transaction.","woocommerce-payments"),(0,de.__)("Transaction unauthorized","woocommerce-payments"),(0,de.__)("If you believe the dispute is invalid, you can challenge it by submitting the appropriate evidence using the response forms on the next screen.","woocommerce-payments"),(0,de.__)("If you believe the payment was actually made using a stolen credit card, you will need to accept the dispute. The credit card networks place liability for accepting unauthorized transactions with you, the business.","woocommerce-payments"),(0,de.__)("This is the most common reason for a dispute, and happens when a cardholder claims that they didn’t authorize the payment. This can happen if the card was lost or stolen and used to make an unauthorized transaction. It can also happen if the cardholder doesn’t recognize the payment as it appears on the billing statement from their card issuer.","woocommerce-payments"),(0,de.__)("Provide adequate payment and order details so that a legitimate customer recognizes it, or proves to the card issuer that their cardholder authorized the transaction.","woocommerce-payments"),(0,de.__)("Try to get in touch with your customer. Sometimes people forget about payments they make or don’t recognize the way they appear on their card statement. If this is the case, ask them to contact their card issuer and let them know they no longer dispute the transaction.","woocommerce-payments"),(0,de.__)("Even if your customer agrees to withdraw the dispute, you must still submit appropriate evidence using the forms on the next screen. Simply saying that your customer is going to withdraw the dispute is not sufficient evidence.","woocommerce-payments"),(0,de.__)("It may be more efficient—and provide a better customer experience—to accept an accidental dispute and charge the customer again, if appropriate. Even when a dispute is withdrawn, it usually takes approximately 75 days to be finalized. Remember, it doesn’t matter to the card networks whether you win or lose a dispute; what matters is how many disputes a business receives, regardless of how many disputes are won.","woocommerce-payments"),(0,de.__)("The cardholder claims this is an unauthorized transaction.","woocommerce-payments"),(0,de.__)("General","woocommerce-payments"),(0,de.__)("If you believe the dispute is invalid, you can challenge it by submitting the appropriate evidence using the response forms on the next screen.","woocommerce-payments"),(0,de.__)("This is an uncategorized dispute, so you should contact the customer for additional details to find out why the payment was disputed.","woocommerce-payments"),(0,de.__)("Incorrect account details","woocommerce-payments"),(0,de.__)("Insufficient funds","woocommerce-payments"),(0,de.__)("Product not received","woocommerce-payments"),(0,de.__)("If you believe the dispute is invalid, you can challenge it by submitting the appropriate evidence using the response forms on the next screen.","woocommerce-payments"),(0,de.__)("If you can not prove the customer received their product or service, you should accept the dispute. You cannot issue a refund while a payment is being disputed. The credit card networks place liability for accepting disputed payments with you, the business.","woocommerce-payments"),(0,de.__)("The customer claims they did not receive the products or services purchased.","woocommerce-payments"),(0,de.__)("Prove that the customer received a physical product or offline service, or made use of a digital product or online service. This must have occurred prior to the date the dispute was initiated.","woocommerce-payments"),(0,de.__)("First, get in touch with your customer. Understanding why they filed the dispute will be important for helping make sure your customer gets the product and will give you critical information to prevent this from happening to others.","woocommerce-payments"),(0,de.__)("Even if your customer agrees to withdraw the dispute, you must still submit appropriate evidence using the forms on the next screen. Simply saying that your customer is going to withdraw the dispute is not sufficient evidence.","woocommerce-payments"),(0,de.__)("The cardholder claims they did not receive the product.","woocommerce-payments"),(0,de.__)("Product unacceptable","woocommerce-payments"),(0,de.__)("If you believe the dispute is invalid, you can challenge it by submitting the appropriate evidence using the response forms on the next screen.","woocommerce-payments"),(0,de.__)("If you can not prove the customer received their product or service as described, you should accept the dispute. You cannot issue a refund while a payment is being disputed. The credit card networks place liability for accepting disputed payments with you, the business.","woocommerce-payments"),(0,de.__)("The product or service was received but was defective, damaged, or not as described.","woocommerce-payments"),(0,de.__)("Demonstrate that the product or service was delivered as described at the time of purchase.","woocommerce-payments"),(0,de.__)("If the product or service is as described, provide specific information (invoice, contract, etc.) to refute the cardholder’s claims. Quality disputes are where the customer does not agree with the condition of merchandise or service received (e.g., a car repair situation or quality of a hotel room). There may be instances where you will need to obtain a neutral third-party opinion to help corroborate your claim against the cardholder. Provide as much specific information and documentation as possible to refute the cardholder’s claims. It is recommended that you address each point that the cardholder has made.","woocommerce-payments"),(0,de.__)("If the customer has not yet returned the product or canceled the service, provide specific information to that effect. You should double-check your incoming shipping records to verify that you have not received a return before you respond. If you have processed a credit or reversal for this transaction, provide evidence of this which includes the amount and date processed.","woocommerce-payments"),(0,de.__)("For products that have been repaired or replaced, provide evidence that the cardholder agreed to a repair or replacement, it has been received by the customer, and the repair or replacement has not since been disputed.","woocommerce-payments"),(0,de.__)("If your customer made no attempt to return the product or cancel the service, or if you provided a replacement product or service, make sure to note that as well.","woocommerce-payments"),(0,de.__)("If the customer withdraws their dispute you should still submit evidence using the forms on the next screen. Be sure to provide a letter or email from the cardholder stating that they are no longer in dispute.","woocommerce-payments"),(0,de.__)("The cardholder claims the product was unacceptable.","woocommerce-payments"),(0,de.__)("Subscription canceled","woocommerce-payments"),(0,de.__)("If you believe the dispute is invalid, you can challenge it by submitting the appropriate evidence using the response forms on the next screen.","woocommerce-payments"),(0,de.__)("If you can not prove the customer’s subscription was canceled, and or they did not follow your cancellation policy, you should accept the dispute. You cannot issue a refund while a payment is being disputed. The credit card networks place liability for accepting disputed payments with you, the business.","woocommerce-payments"),(0,de.__)("The customer claims that you continued to charge them after a subscription was canceled.","woocommerce-payments"),(0,de.__)("Prove that the subscription was still active and that the customer was aware of, and did not follow, your cancellation procedure.","woocommerce-payments"),(0,de.__)("First, get in touch with your customer. If you understand what they believe happened, there is a chance for you to explain the misunderstanding or to make it right. ","woocommerce-payments"),(0,de.__)("Even if your customer agrees to withdraw the dispute, you must still submit appropriate evidence using the forms on the next screen. Simply saying that your customer is going to withdraw the dispute is not sufficient evidence.","woocommerce-payments"),(0,de.__)("Otherwise, use the forms on the next screen to submit evidence that the subscription was still active and that the customer was aware of, and did not follow, your cancellation procedure.","woocommerce-payments"),(0,de.__)("The cardholder claims a subscription was canceled.","woocommerce-payments"),(0,de.__)("Unrecognized","woocommerce-payments"),(0,de.__)("If you believe the dispute is invalid, you can challenge it by submitting the appropriate evidence using the response forms on the next screen.","woocommerce-payments"),(0,de.__)("If you can not prove the customer’s subscription was canceled, and or they did not follow your cancellation policy, you should accept the dispute. You cannot issue a refund while a payment is being disputed. The credit card networks place liability for accepting disputed payments with you, the business.","woocommerce-payments"),(0,de.__)("The customer doesn’t recognize the payment appearing on their card statement.","woocommerce-payments"),(0,de.__)("Get your customer to withdraw the dispute by helping them identify the payment. Otherwise, challenge the dispute with appropriate evidence that proves the purchase was legitimate. ","woocommerce-payments"),(0,de.__)("First, try to get in touch with your customer. Sometimes people forget about payments they make or don’t recognize the way they appear on their card statement. If this is the case, ask them to contact their card issuer and let them know they no longer dispute the transaction. Even if your customer agrees to withdraw the dispute, you must still submit appropriate evidence. Simply saying that your customer is going to withdraw the dispute is not sufficient evidence.","woocommerce-payments"),(0,de.__)("It may be more efficient—and provide a better customer experience—to accept an accidental dispute and charge the customer again, if appropriate. Even when a dispute is withdrawn, it usually takes approximately 75 days to be finalized. Remember, it doesn’t matter to the card networks whether you win or lose a dispute; what matters is how many disputes a business receives, regardless of how many disputes are won.","woocommerce-payments"),(0,de.__)("Non-compliant","woocommerce-payments"),(0,de.__)("Your customer’s bank claims this payment violates Visa’s rules.","woocommerce-payments"),(0,de.__)("This transaction is being reviewed under Visa’s network compliance rules.","woocommerce-payments"),{warning_needs_response:(0,de.__)("Inquiry: Response needed","woocommerce-payments"),warning_under_review:(0,de.__)("Inquiry: Under review","woocommerce-payments"),warning_closed:(0,de.__)("Inquiry: Closed","woocommerce-payments"),needs_response:(0,de.__)("Response needed","woocommerce-payments"),under_review:(0,de.__)("Under review","woocommerce-payments"),charge_refunded:(0,de.__)("Charge refunded","woocommerce-payments"),won:(0,de.__)("Won","woocommerce-payments"),lost:(0,de.__)("Lost","woocommerce-payments")}),tr=(Object.entries(er).map((([e,t])=>({label:t,value:e}))).filter((function(e){return null!=e})),["needs_response","warning_needs_response"]),rr=((0,de.__)("Dispute currency","woocommerce-payments"),(0,de.__)("All currencies","woocommerce-payments"),(0,de.__)("Show","woocommerce-payments"),(0,de.__)("Needs response","woocommerce-payments"),(0,de.__)("All disputes","woocommerce-payments"),(0,de.__)("Advanced filters","woocommerce-payments"),(0,Xt.getSetting)("wcVersion")),or=parseFloat(rr),ar=(or<7.8?(0,de.__)("Disputes match {{select /}} filters","woocommerce-payments"):(0,de.__)("Disputes match <select /> filters","woocommerce-payments"),(0,de.__)("Disputed on date","woocommerce-payments"),(0,de.__)("Remove dispute date filter","woocommerce-payments"),(0,de.__)("Select a dispute date filter match","woocommerce-payments"),or<7.8?(0,de.__)("{{title}}Date{{/title}} {{rule /}} {{filter /}}","woocommerce-payments"):(0,de.__)("<title>Date</title> <rule /> <filter />","woocommerce-payments"),(0,de.__)("Select a dispute date","woocommerce-payments"),(0,de.__)("Before","woocommerce-payments"),(0,de.__)("After","woocommerce-payments"),(0,de.__)("Between","woocommerce-payments"),(0,de.__)("Status","woocommerce-payments"),(0,de.__)("Remove dispute status filter","woocommerce-payments"),(0,de.__)("Select a dispute status filter match","woocommerce-payments"),or<7.8?(0,de.__)("{{title}}Status{{/title}} {{rule /}} {{filter /}}","woocommerce-payments"):(0,de.__)("<title>Status</title> <rule /> <filter />","woocommerce-payments"),(0,de.__)("Select a dispute status","woocommerce-payments"),(0,de._x)("Is","dispute status","woocommerce-payments"),(0,de._x)("Is not","dispute status","woocommerce-payments"),e=>({user_email:e.userEmail,match:e.match,store_currency_is:e.storeCurrencyIs,date_before:Oe(e.dateBefore,!0),date_after:Oe(e.dateAfter),date_between:e.dateBetween&&[Oe(e.dateBetween[0]),Oe(e.dateBetween[1],!0)],search:"awaiting_response"===e.filter?tr:e.search,status_is:e.statusIs,status_is_not:e.statusIsNot,locale:e.locale})),nr=`${H}/disputes/download`;function sr(e){return(0,ge.addQueryArgs)(nr,ar(e))}function*ir(e){const t=(0,ge.addQueryArgs)(`${H}/disputes/${e}`);try{const e=yield(0,$.apiFetch)({path:t});yield Qt(e)}catch(t){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving dispute.","woocommerce-payments")),yield Wt(e,void 0,t)}}function*cr(e){const t=(0,ge.addQueryArgs)(`${H}/disputes`,{page:e.paged,pagesize:e.perPage,sort:(0,G.snakeCase)(e.orderBy),direction:e.order,...ar(e)});try{const r=yield(0,$.apiFetch)({path:t})||{};yield Zt(e,r.data)}catch(e){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving disputes.","woocommerce-payments"))}}function*dr(e){const t=(0,ge.addQueryArgs)(`${H}/disputes/summary`,{page:e.paged,pagesize:e.perPage,...ar(e)});try{const r=yield(0,$.apiFetch)({path:t});yield Kt(e,r)}catch(e){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving the summary of disputes.","woocommerce-payments"))}}const ur={SET_SETTINGS:"SET_SETTINGS",SET_SETTINGS_VALUES:"SET_SETTINGS_VALUES",SET_IS_SAVING_SETTINGS:"SET_IS_SAVING_SETTINGS",SET_SELECTED_PAYMENT_METHOD:"SET_SELECTED_PAYMENT_METHOD",SET_UNSELECTED_PAYMENT_METHOD:"SET_UNSELECTED_PAYMENT_METHOD"},_r={isDirty:!1,isSaving:!1,savingError:null,data:{}},pr={},mr=[],lr=e=>e&&e.settings||pr,yr=e=>lr(e).data||pr,hr=e=>yr(e).duplicated_payment_method_ids||pr,Sr=e=>yr(e).is_wcpay_enabled||!1,Er=e=>yr(e).enabled_payment_method_ids||mr,gr=e=>yr(e).available_payment_method_ids||mr,Tr=e=>yr(e).payment_method_statuses||pr,Rr=e=>lr(e).isSaving||!1,wr=e=>lr(e).isDirty||!1,fr=e=>yr(e).account_statement_descriptor||"",Or=e=>yr(e).account_statement_descriptor_kanji||"",vr=e=>yr(e).account_statement_descriptor_kana||"",br=e=>yr(e).account_business_support_email||"",Ir=e=>yr(e).account_business_support_phone||"",Ar=e=>yr(e).account_domestic_currency||"",Nr=e=>yr(e).deposit_schedule_interval||"",Cr=e=>yr(e).deposit_schedule_weekly_anchor||"",Pr=e=>yr(e).deposit_schedule_monthly_anchor||"",Dr=e=>yr(e).deposit_delay_days||"7",Mr=e=>yr(e).deposit_completed_waiting_period||!1,Fr=e=>yr(e).deposit_status||"",Ur=e=>yr(e).deposit_restrictions||"",Lr=e=>yr(e).is_manual_capture_enabled||!1,kr=e=>yr(e).is_test_mode_enabled||!1,xr=e=>yr(e).is_test_mode_onboarding||!1,jr=e=>yr(e).is_dev_mode_enabled||!1,Yr=e=>yr(e).is_payment_request_enabled||!1,zr=e=>yr(e).is_apple_google_pay_in_payment_methods_options_enabled||!1,qr=e=>yr(e).is_debug_log_enabled||!1,Br=e=>yr(e).is_multi_currency_enabled||!1,$r=e=>yr(e).payment_request_button_type||"",Hr=e=>yr(e).payment_request_button_size||"",Vr=e=>yr(e).payment_request_button_theme||"",Gr=e=>{const t=yr(e)?.payment_request_button_border_radius;return 0===t||"0"===t||t?t:"undefined"==typeof wcpaySettings?4:parseInt(window?.wcpaySettings?.defaultExpressCheckoutBorderRadius||4,10)},Qr=e=>yr(e).is_saved_cards_enabled||!1,Wr=e=>lr(e).savingError,Zr=e=>yr(e).is_card_present_eligible||!1,Kr=e=>yr(e).is_wcpay_subscriptions_enabled||!1,Jr=e=>yr(e).is_wcpay_subscriptions_eligible||!1,Xr=e=>yr(e).is_subscriptions_plugin_active||!1,eo=e=>yr(e).is_woopay_enabled||!1,to=e=>yr(e).is_amazon_pay_enabled||!1,ro=e=>yr(e).is_woopay_global_theme_support_enabled||!1,oo=e=>yr(e).woopay_custom_message||"",ao=e=>yr(e).woopay_store_logo||"",no=e=>yr(e).current_protection_level||"basic",so=e=>yr(e).advanced_fraud_protection_settings||mr,io=e=>yr(e).show_woopay_incompatibility_notice||!1,co=e=>yr(e).is_stripe_billing_enabled||!1,uo=e=>yr(e).is_migrating_stripe_billing||!1,_o=e=>yr(e).stripe_billing_subscription_count||0,po=e=>yr(e).stripe_billing_migrated_count||0,mo=e=>yr(e).account_communications_email||"",lo=e=>yr(e).express_checkout_product_methods||mr,yo=e=>yr(e).express_checkout_cart_methods||mr,ho=e=>yr(e).express_checkout_checkout_methods||mr,So=window.wp.apiFetch;var Eo=r.n(So);function go(e){return{type:ur.SET_SETTINGS_VALUES,payload:e}}function To(e){return go({is_saved_cards_enabled:e})}function Ro(e){return go({is_card_present_eligible:e})}function wo(e){return go({payment_request_button_type:e})}function fo(e){return go({payment_request_button_size:e})}function Oo(e){return go({payment_request_button_theme:e})}function vo(e){return go({payment_request_button_border_radius:e})}function bo(e){return{type:ur.SET_SETTINGS,data:e}}function Io(e){return go({is_wcpay_enabled:e})}function Ao(e){return go({is_payment_request_enabled:e})}function No(e){return go({is_apple_google_pay_in_payment_methods_options_enabled:e})}function Co(e){return go({enabled_payment_method_ids:[...e]})}function Po(e,t){return{type:ur.SET_IS_SAVING_SETTINGS,isSaving:e,error:t}}function Do(e){return{type:ur.SET_SELECTED_PAYMENT_METHOD,id:e}}function Mo(e){return{type:ur.SET_UNSELECTED_PAYMENT_METHOD,id:e}}function Fo(e){return go({is_manual_capture_enabled:e})}function Uo(e){return go({is_test_mode_enabled:e})}function Lo(e){return go({is_debug_log_enabled:e})}function ko(e){return go({is_multi_currency_enabled:e})}function xo(e){return go({is_wcpay_subscriptions_enabled:e})}function jo(e){return go({account_statement_descriptor:e})}function Yo(e){return go({account_statement_descriptor_kanji:e})}function zo(e){return go({account_statement_descriptor_kana:e})}function qo(e){return go({account_business_support_email:e})}function Bo(e){return go({account_business_support_phone:e})}function $o(e){return go({deposit_schedule_interval:e})}function Ho(e){return go({deposit_schedule_weekly_anchor:e})}function Vo(e){return go({deposit_schedule_monthly_anchor:""===e?null:parseInt(e,10)})}function*Go(){let e=null;try{const e=(0,B.select)(V).getSettings();yield Po(!0,null);const t=yield(0,$.apiFetch)({path:`${H}/settings`,method:"post",data:e});yield go({payment_method_statuses:t.data.payment_method_statuses}),yield(0,B.dispatch)("core/notices").createSuccessNotice((0,de.__)("Settings saved.","woocommerce-payments"))}catch(t){e=t,yield(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error saving settings.","woocommerce-payments")),e.server_error&&(yield(0,B.dispatch)("core/notices").createErrorNotice(e.server_error))}finally{yield Po(!1,e)}return null===e}function Qo(e){return go({is_woopay_enabled:e})}function Wo(e){return go({is_amazon_pay_enabled:e})}function Zo(e){return go({is_woopay_global_theme_support_enabled:e})}function Ko(e){return go({woopay_custom_message:e})}function Jo(e){return go({woopay_store_logo:e})}function Xo(e){return go({current_protection_level:e})}function ea(e){return go({advanced_fraud_protection_settings:e})}function ta(e){return go({is_stripe_billing_enabled:e})}function ra(e){return go({account_communications_email:e})}function oa(e){return go({express_checkout_product_methods:[...e]})}function aa(e){return go({express_checkout_cart_methods:[...e]})}function na(e){return go({express_checkout_checkout_methods:[...e]})}function*sa(){try{yield(0,B.dispatch)(V).startResolution("scheduleStripeBillingMigration"),yield(0,$.apiFetch)({path:`${H}/settings/schedule-stripe-billing-migration`,method:"post"})}catch(e){yield(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error starting the Stripe Billing migration.","woocommerce-payments"))}yield(0,B.dispatch)(V).finishResolution("scheduleStripeBillingMigration")}function ia(e,t){return Eo()({path:`${H}/settings/${e}`,method:"post",data:{value:t}}).catch((()=>{(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error saving option","woocommerce-payments"))}))}function*ca(){const e=`${H}/settings`;try{const t=yield(0,$.apiFetch)({path:e});yield bo(t)}catch(e){yield(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error retrieving settings.","woocommerce-payments"))}}const da=e=>()=>{const{updateExpressCheckoutProductMethods:t,updateExpressCheckoutCartMethods:r,updateExpressCheckoutCheckoutMethods:o}=(0,B.useDispatch)(V),a=(0,B.useSelect)((e=>e(V).getExpressCheckoutProductMethods())),n=(0,B.useSelect)((e=>e(V).getExpressCheckoutCartMethods())),s=(0,B.useSelect)((e=>e(V).getExpressCheckoutCheckoutMethods())),i={product:a,cart:n,checkout:s},c={product:t,cart:r,checkout:o};return[[a.includes(e)&&"product",n.includes(e)&&"cart",s.includes(e)&&"checkout"].filter(Boolean),(t,r)=>{c[t](r?[...i[t],e]:i[t].filter((t=>t!==e)))}]},ua=(da("payment_request"),da("woopay"),da("amazon_pay"),{SET_CARD_READER_STATS:"SET_CARD_READER_STATS",SET_ERROR_FOR_CARD_READER_STATS:"SET_ERROR_FOR_CARD_READER_STATS",SET_READERS:"SET_READERS",SET_ERROR_FOR_CARD_READERS:"SET_ERROR_FOR_CARD_READERS"}),_a=(e,t)=>e.readers[t]&&e.readers[t].data?e.readers[t].data:{},pa=(e,t)=>e.readers[t]&&e.readers[t].error?e.readers[t].error:{},ma=e=>e.readers&&e.readers.list?e.readers.list:{};function la(e,t){return{type:ua.SET_CARD_READER_STATS,id:e,data:t}}function ya(e,t){return{type:ua.SET_READERS,query:e,data:t}}function ha(e,t,r){return{type:ua.SET_CARD_ERROR_FOR_READER_STATS,id:e,data:t,error:r}}function Sa(e,t){return{type:ua.SET_CARD_ERROR_FOR_READERS,data:e,error:t}}function*Ea(e,t){try{const r=yield(0,$.apiFetch)({path:`${H}/readers/charges/${t}`});yield la(e,r)}catch(t){yield ha(e,null,t)}}function*ga(e){const t=(0,ge.addQueryArgs)(`${H}/readers`,{limit:e.limit});try{const r=yield(0,$.apiFetch)({path:t})||{};yield ya(e,r)}catch(e){yield Sa(null,e)}}var Ta;!function(e){e.SET_ACTIVE_LOAN_SUMMARY="SET_ACTIVE_LOAN_SUMMARY",e.SET_ERROR_FOR_ACTIVE_LOAN_SUMMARY="SET_ERROR_FOR_ACTIVE_LOAN_SUMMARY",e.SET_LOANS="SET_LOANS",e.SET_ERROR_FOR_LOANS="SET_ERROR_FOR_LOANS"}(Ta||(Ta={}));const Ra=Ta,wa={},fa=e=>e&&e.capital||{},Oa=e=>fa(e).summary,va=e=>fa(e).summaryError,ba=e=>fa(e).loans||[],Ia=e=>fa(e).loansError;function Aa(e){return{type:Ra.SET_ACTIVE_LOAN_SUMMARY,data:e}}function Na(e){return{type:Ra.SET_ERROR_FOR_ACTIVE_LOAN_SUMMARY,error:e}}function Ca(e){return{type:Ra.SET_LOANS,data:e.data}}function Pa(e){return{type:Ra.SET_ERROR_FOR_LOANS,error:e}}function*Da(){const e=`${H}/capital/active_loan_summary`;try{const t=yield(0,$.apiFetch)({path:e});yield Aa(t)}catch(e){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving the active loan summary.","woocommerce-payments")),yield Na(e)}}function*Ma(){const e=`${H}/capital/loans`;try{const t=yield(0,$.apiFetch)({path:e});yield Ca(t)}catch(e){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving the active loan summary.","woocommerce-payments")),yield Pa(e)}}const Fa={SET_DOCUMENTS:"SET_DOCUMENTS",SET_ERROR_FOR_DOCUMENTS:"SET_ERROR_FOR_DOCUMENTS",SET_DOCUMENTS_SUMMARY:"SET_DOCUMENTS_SUMMARY",SET_ERROR_FOR_DOCUMENTS_SUMMARY:"SET_ERROR_FOR_DOCUMENTS_SUMMARY"},Ua={summary:{}},La=e=>e&&e.documents||{},ka=(e,t)=>{const r=W(t);return La(e)[r]||{}},xa=(e,t)=>ka(e,t).data||[],ja=(e,t)=>ka(e,t).error||{},Ya=(e,t)=>{const r=W(t);return La(e).summary[r]||{}},za=(e,t)=>Ya(e,t).data||{},qa=(e,t)=>Ya(e,t).error||{};function Ba(e,t){return{type:Fa.SET_DOCUMENTS,query:e,data:t}}function $a(e,t,r){return{type:Fa.SET_ERROR_FOR_DOCUMENTS,query:e,data:t,error:r}}function Ha(e,t){return{type:Fa.SET_DOCUMENTS_SUMMARY,query:e,data:t}}function Va(e,t,r){return{type:Fa.SET_ERROR_FOR_DOCUMENTS_SUMMARY,query:e,data:t,error:r}}const Ga=e=>({user_email:e.userEmail,match:e.match,date_before:Oe(e.dateBefore,!0),date_after:Oe(e.dateAfter),date_between:e.dateBetween&&[Oe(e.dateBetween[0]),Oe(e.dateBetween[1],!0)],type_is:e.typeIs,type_is_not:e.typeIsNot});function*Qa(e){const t=(0,ge.addQueryArgs)(`${H}/documents`,{page:e.paged,pagesize:e.perPage,sort:e.orderby,direction:e.order,...Ga(e)});try{const r=yield(0,$.apiFetch)({path:t});yield Ba(e,r.data||[])}catch(t){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving documents.","woocommerce-payments")),yield $a(e,null,t)}}function*Wa(e){const t=(0,ge.addQueryArgs)(`${H}/documents/summary`,Ga(e));try{const r=yield(0,$.apiFetch)({path:t});yield Ha(e,r)}catch(t){yield Va(e,null,t)}}const Za={},Ka=({paymentIntents:e},t)=>{const r=null==e?void 0:e[t];return(null==r?void 0:r.data)||{}},Ja=({paymentIntents:e},t)=>{const r=null==e?void 0:e[t];return(null==r?void 0:r.error)||{}},Xa={SET_AUTHORIZATIONS:"SET_AUTHORIZATIONS",SET_AUTHORIZATION:"SET_AUTHORIZATION",SET_ERROR_FOR_AUTHORIZATIONS:"SET_ERROR_FOR_AUTHORIZATIONS",SET_AUTHORIZATIONS_SUMMARY:"SET_AUTHORIZATIONS_SUMMARY",SET_ERROR_FOR_AUTHORIZATIONS_SUMMARY:"SET_ERROR_FOR_AUTHORIZATIONS_SUMMARY",SET_IS_REQUESTING_AUTHORIZATION:"SET_IS_REQUESTING_AUTHORIZATION"},en={summary:{},byId:{},isRequesting:!1},tn=e=>e&&e.authorizations||{},rn=(e,t)=>{const r=W(t);return tn(e)[r]||{}},on=(e,t)=>rn(e,t).data||[],an=(e,t)=>(tn(e).byId||{})[t],nn=(e,t)=>rn(e,t).error||{},sn=(e,t)=>{const r=W(t);return tn(e).summary[r]||{}},cn=(e,t)=>sn(e,t).data||{},dn=(e,t)=>sn(e,t).error||{},un=e=>tn(e).isRequesting,_n=e=>{var t;const r={wcpay_missing_order:(0,de.__)("The order could not be found.","woocommerce-payments"),wcpay_refunded_order_uncapturable:(0,de.__)("Payment cannot be processed for partially or fully refunded orders.","woocommerce-payments"),wcpay_intent_order_mismatch:(0,de.__)("The payment cannot be processed due to a mismatch with order details.","woocommerce-payments"),wcpay_payment_uncapturable:(0,de.__)("This payment cannot be processed in its current state.","woocommerce-payments"),wcpay_capture_error:e=>{var t;return"amount_too_small"===(null===(t=e.data)||void 0===t?void 0:t.error_type)?(e=>{var t,r,o,a;if(!(null===(r=null===(t=e.data)||void 0===t?void 0:t.extra_details)||void 0===r?void 0:r.minimum_amount)||!(null===(a=null===(o=e.data)||void 0===o?void 0:o.extra_details)||void 0===a?void 0:a.minimum_amount_currency))return(0,de.__)("The payment amount is too small to be processed.","woocommerce-payments");const n=e.data.extra_details.minimum_amount_currency,s=he(e.data.extra_details.minimum_amount,n);return(0,de.sprintf)((0,de.__)("The minimum amount that can be processed is %1$s %2$s.","woocommerce-payments"),s,n.toUpperCase())})(e):(0,de.__)("The payment capture failed to complete.","woocommerce-payments")},wcpay_cancel_error:(0,de.__)("The payment cancellation failed to complete.","woocommerce-payments"),wcpay_server_error:(0,de.__)("An unexpected error occurred. Please try again later.","woocommerce-payments")},o=r[null!==(t=e.code)&&void 0!==t?t:""];return"function"==typeof o?o(e):null!=o?o:(0,de.__)("Unable to process the payment. Please try again later.","woocommerce-payments")};function pn(e,t){return{type:Xa.SET_AUTHORIZATIONS,data:t,query:e}}function mn(e,t){return{type:Xa.SET_ERROR_FOR_AUTHORIZATIONS,query:e,error:t}}function ln(e){return{type:Xa.SET_AUTHORIZATION,data:e}}function yn(e,t){return{type:Xa.SET_AUTHORIZATIONS_SUMMARY,data:t,query:e}}function hn(e){return{type:Xa.SET_IS_REQUESTING_AUTHORIZATION,data:e}}function*Sn(e,t){try{yield B.controls.dispatch(V,"startResolution","getAuthorization",[e]),yield B.controls.dispatch(V,"setIsRequestingAuthorization",!0);const r=yield(0,$.apiFetch)({path:`/wc/v3/payments/orders/${t}/capture_authorization`,method:"post",data:{payment_intent_id:e}}),o={payment_intent_id:r.id,captured:"succeeded"===r.status};yield ln(o),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getAuthorizations"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getAuthorizationsSummary"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getFraudOutcomeTransactions"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getFraudOutcomeTransactionsSummary"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getTimeline"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getPaymentIntent"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getTransactions"),yield B.controls.dispatch("core/notices","createSuccessNotice",(0,de.sprintf)((0,de.__)("Payment for order #%s captured successfully.","woocommerce-payments"),t))}catch(e){const r=(0,de.sprintf)((0,de.__)("There has been an error capturing the payment for order #%s.","woocommerce-payments"),t),o=_n(e);yield B.controls.dispatch("core/notices","createErrorNotice",`${r} ${o}`)}finally{yield B.controls.dispatch(V,"finishResolution","getAuthorization",[e]),yield B.controls.dispatch(V,"setIsRequestingAuthorization",!1)}}function*En(e,t){try{yield B.controls.dispatch(V,"startResolution","getAuthorization",[e]),yield B.controls.dispatch(V,"setIsRequestingAuthorization",!0);const r=yield(0,$.apiFetch)({path:`/wc/v3/payments/orders/${t}/cancel_authorization`,method:"post",data:{payment_intent_id:e}}),o={payment_intent_id:r.id,captured:"succeeded"===r.status};yield ln(o),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getAuthorizations"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getAuthorizationsSummary"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getFraudOutcomeTransactions"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getFraudOutcomeTransactionsSummary"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getTimeline"),yield B.controls.dispatch(V,"invalidateResolutionForStoreSelector","getPaymentIntent"),yield B.controls.dispatch("core/notices","createSuccessNotice",(0,de.sprintf)((0,de.__)("Payment for order #%s canceled successfully.","woocommerce-payments"),t))}catch(e){const r=(0,de.sprintf)((0,de.__)("There has been an error canceling the payment for order #%s.","woocommerce-payments"),t),o=_n(e);yield B.controls.dispatch("core/notices","createErrorNotice",`${r} ${o}`)}finally{yield B.controls.dispatch(V,"finishResolution","getAuthorization",[e]),yield B.controls.dispatch(V,"setIsRequestingAuthorization",!1)}}function gn(e,t){return{type:Xa.SET_ERROR_FOR_AUTHORIZATIONS_SUMMARY,query:e,error:t}}function*Tn(e){var t;let{paged:r=1,per_page:o=25,orderby:a="created",order:n="asc"}=e;"capture_by"===a&&(a="created");const s=(0,ge.addQueryArgs)(`${H}/authorizations`,{page:r,pagesize:o,sort:a,direction:n});try{const r=yield(0,$.apiFetch)({path:s});yield pn(e,null!==(t=r.data)&&void 0!==t?t:[])}catch(t){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving uncaptured transactions.","woocommerce-payments")),yield mn(e,t)}}function*Rn(e){try{if(e){const t=yield(0,$.apiFetch)({path:`${H}/authorizations/${e}`}),{is_captured:r,payment_intent_id:o,created:a}=t;yield ln({payment_intent_id:o,captured:r,created:a})}}catch(e){"authorization_missing"!==e.code&&(yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving authorization.","woocommerce-payments")))}}function*wn(e){const t=(0,ge.addQueryArgs)(`${H}/authorizations/summary`,{pagesize:e.per_page,sort:e.orderby,direction:e.order,page:e.paged});try{const r=yield(0,$.apiFetch)({path:t});yield yn(e,null!=r?r:[])}catch(t){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving uncaptured transactions.","woocommerce-payments")),yield gn(e,t)}}var fn;!function(e){e.SET_FILE="SET_FILE",e.SET_ERROR_FOR_FILES="SET_ERROR_FOR_FILES"}(fn||(fn={}));const On=fn,vn={},bn=({files:e},t)=>{const r=null==e?void 0:e[t];return(null==r?void 0:r.data)||{}},In=({files:e},t)=>{const r=null==e?void 0:e[t];return(null==r?void 0:r.error)||{}};function An(e,t){return{type:On.SET_FILE,id:e,data:t}}function Nn(e,t){return{type:On.SET_ERROR_FOR_FILES,id:e,error:t}}function*Cn(e){try{const t=yield(0,$.apiFetch)({path:`${H}/file/${e}/details`});yield An(e,t)}catch(t){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving file.","woocommerce-payments")),yield Nn(e,t)}}var Pn;!function(e){e.SET_PM_PROMOTIONS="SET_PM_PROMOTIONS",e.SET_ERROR_FOR_PM_PROMOTIONS="SET_ERROR_FOR_PM_PROMOTIONS"}(Pn||(Pn={}));const Dn={pmPromotions:void 0,pmPromotionsError:void 0},Mn=e=>{var t,r;return null!==(r=null===(t=e.pmPromotions)||void 0===t?void 0:t.pmPromotions)&&void 0!==r?r:[]},Fn=e=>{var t;return null===(t=e.pmPromotions)||void 0===t?void 0:t.pmPromotionsError};function Un(e){return function(e){return"object"==typeof e&&null!==e&&"code"in e&&"string"==typeof e.code}(e)?e:{code:"unknown_error"}}function Ln(e){return{type:Pn.SET_PM_PROMOTIONS,data:e}}function kn(e){return{type:Pn.SET_ERROR_FOR_PM_PROMOTIONS,error:e}}function*xn(e){const t=`${H}/pm-promotions/${e}/activate`;try{yield(0,$.apiFetch)({path:t,method:"POST"}),yield B.controls.dispatch("core/notices","createSuccessNotice",(0,de.__)("Promotion activated successfully!","woocommerce-payments")),yield B.controls.dispatch("wc/payments","invalidateResolution","getPmPromotions",[])}catch(e){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error activating promotion. Please try again.","woocommerce-payments")),yield B.controls.dispatch("wc/payments","updateErrorForPmPromotions",Un(e))}}function*jn(e){const t=`${H}/pm-promotions/${e}/dismiss`;try{yield(0,$.apiFetch)({path:t,method:"POST"}),yield B.controls.dispatch("core/notices","createSuccessNotice",(0,de.__)("Promotion dismissed.","woocommerce-payments")),yield B.controls.dispatch("wc/payments","invalidateResolution","getPmPromotions",[])}catch(e){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error dismissing promotion. Please try again.","woocommerce-payments")),yield B.controls.dispatch("wc/payments","updateErrorForPmPromotions",Un(e))}}function Yn(e){if("object"!=typeof e||null===e)return!1;const t=e;return"string"==typeof t.id&&"string"==typeof t.promo_id&&"string"==typeof t.payment_method&&"string"==typeof t.payment_method_title&&"string"==typeof t.type&&("spotlight"===t.type||"badge"===t.type)&&"string"==typeof t.title&&"string"==typeof t.description&&"string"==typeof t.cta_label&&"string"==typeof t.tc_url&&"string"==typeof t.tc_label}function*zn(){const e=`${H}/pm-promotions`;try{const t=yield(0,$.apiFetch)({path:e});if(r=t,!Array.isArray(r)||!r.every(Yn))throw new Error("Invalid promotions data received from API");yield B.controls.dispatch("wc/payments","updatePmPromotions",t)}catch(e){yield B.controls.dispatch("core/notices","createErrorNotice",(0,de.__)("Error retrieving promotions. Please try again later.","woocommerce-payments")),yield B.controls.dispatch("wc/payments","updateErrorForPmPromotions",(t=e,function(e){return"object"==typeof e&&null!==e&&"code"in e}(t)?t:{code:"unknown_error"}))}var t,r}(0,B.registerStore)(V,{reducer:(0,B.combineReducers)({deposits:(e=Z,{type:t,query:r={},data:o=[],error:a})=>{const n=W(r);switch(t){case Q.SET_DEPOSIT:return{...e,byId:{...e.byId,[o.id]:o}};case Q.SET_ALL_DEPOSITS_OVERVIEWS:return{...e,overviews:{...e.overviews,data:o}};case Q.SET_ERROR_FOR_ALL_DEPOSITS_OVERVIEWS:return{...e,overviews:{...e.overviews,error:a}};case Q.SET_DEPOSITS:return{...e,byId:{...e.byId,...(0,G.keyBy)(o,"id")},queries:{...e.queries,[n]:{data:(0,G.map)(o,"id")}}};case Q.SET_DEPOSITS_COUNT:return{...e,count:o};case Q.SET_ERROR_FOR_DEPOSIT_QUERY:return{...e,queries:{...e.queries,[n]:{error:a}}};case Q.SET_DEPOSITS_SUMMARY:return{...e,summary:{...e.summary,[n]:{data:o}}};case Q.SET_ERROR_FOR_DEPOSITS_SUMMARY:return{...e,summary:{...e.summary,[n]:{error:a}}};case Q.SET_INSTANT_DEPOSIT:return{...e,instant:{...e.instant,data:o}}}return e},transactions:(e=Be,{type:t,query:r={},data:o=[],error:a,status:n})=>{const s=W(r);switch(t){case qe.SET_TRANSACTIONS:return{...e,[s]:{data:o}};case qe.SET_ERROR_FOR_TRANSACTIONS:return{...e,[s]:{error:a}};case qe.SET_TRANSACTIONS_SUMMARY:return{...e,summary:{...e.summary,[s]:{data:o}}};case qe.SET_ERROR_FOR_TRANSACTIONS_SUMMARY:return{...e,summary:{...e.summary,[s]:{error:a}}};case qe.SET_FRAUD_OUTCOME_TRANSACTIONS:return{...e,fraudProtection:{...e.fraudProtection,[n]:{...e.fraudProtection?.[n]||{},[s]:{data:o}}}};case qe.SET_ERROR_FOR_FRAUD_OUTCOME_TRANSACTIONS:return{...e,fraudProtection:{...e.fraudProtection,[n]:{...e.fraudProtection?.[n]||{},[s]:{error:a}}}};case qe.SET_FRAUD_OUTCOME_TRANSACTIONS_SUMMARY:return{...e,fraudProtection:{...e.fraudProtection,[n]:{...e.fraudProtection?.[n]||{},summary:{...e.fraudProtection?.[n]?.summary||{},[s]:{data:o}}}}};case qe.SET_ERROR_FOR_FRAUD_OUTCOME_TRANSACTIONS_SUMMARY:return{...e,fraudProtection:{...e.fraudProtection,[n]:{...e.fraudProtection?.[n]||{},summary:{...e.fraudProtection?.[n]?.summary||{},[s]:{error:a}}}}}}return e},charges:(e={},{type:t,id:r,data:o,error:a})=>{switch(t){case gt.SET_CHARGE:e={...e,[r]:{...e[r],data:o}};break;case gt.SET_ERROR_FOR_CHARGE:e={...e,[r]:{...e[r],error:a}}}return e},timeline:(e={},{type:t,id:r,data:o,error:a})=>{switch(t){case At.SET_TIMELINE:e={...e,[r]:{data:o}};break;case At.SET_ERROR_FOR_TIMELINE:e={...e,[r]:{...e[r],error:a}}}return e},disputes:(e=Ut,{type:t,query:r={},data:o=[],id:a,error:n})=>{const s=W(r);switch(t){case Ft.SET_DISPUTE:return{...e,byId:{...e.byId,[o.id]:o}};case Ft.SET_ERROR_FOR_DISPUTE:e={...e,byId:{...e.byId,[a]:{error:n}}};break;case Ft.SET_DISPUTES:return{...e,cached:{...e.cached,...(0,G.keyBy)(o,"dispute_id")},queries:{...e.queries,[s]:{data:(0,G.map)(o,"dispute_id")}}};case Ft.SET_DISPUTES_SUMMARY:return{...e,summary:{...e.summary,[s]:{data:o}}}}return e},settings:(e=_r,{type:t,...r})=>{switch(t){case ur.SET_SETTINGS:return{...e,data:r.data,isDirty:!1};case ur.SET_SETTINGS_VALUES:return{...e,savingError:null,isDirty:!0,data:{...e.data,...r.payload}};case ur.SET_IS_SAVING_SETTINGS:return{...e,isDirty:!(!r.isSaving&&!r.error)&&e.isDirty,isSaving:r.isSaving,savingError:r.error};case ur.SET_SELECTED_PAYMENT_METHOD:return{...e,isDirty:!0,data:{...e.data,enabled_payment_method_ids:e.data.enabled_payment_method_ids.concat(r.id)}};case ur.SET_UNSELECTED_PAYMENT_METHOD:return{...e,isDirty:!0,data:{...e.data,enabled_payment_method_ids:e.data.enabled_payment_method_ids.filter((e=>e!==r.id))}}}return e},readers:(e={},{type:t,id:r,data:o,error:a})=>{switch(t){case ua.SET_CARD_READER_STATS:e={...e,[r]:{...e[r],data:o}};break;case ua.SET_ERROR_FOR_CARD_READER_STATS:e={...e,[r]:{...e[r],error:a}};break;case ua.SET_READERS:e={...e,list:o}}return e},capital:(e=wa,t)=>{switch(t.type){case Ra.SET_ACTIVE_LOAN_SUMMARY:return Object.assign(Object.assign({},e),{summary:t.data,summaryError:void 0});case Ra.SET_ERROR_FOR_ACTIVE_LOAN_SUMMARY:return Object.assign(Object.assign({},e),{summary:void 0,summaryError:t.error});case Ra.SET_LOANS:return Object.assign(Object.assign({},e),{loans:t.data,loansError:void 0});case Ra.SET_ERROR_FOR_LOANS:return Object.assign(Object.assign({},e),{loans:void 0,loansError:t.error})}return e},documents:(e=Ua,{type:t,query:r={},data:o=[],error:a})=>{const n=W(r);switch(t){case Fa.SET_DOCUMENTS:return{...e,[n]:{data:o}};case Fa.SET_ERROR_FOR_DOCUMENTS:return{...e,[n]:{error:a}};case Fa.SET_DOCUMENTS_SUMMARY:return{...e,summary:{...e.summary,[n]:{data:o}}};case Fa.SET_ERROR_FOR_DOCUMENTS_SUMMARY:return{...e,summary:{...e.summary,[n]:{error:a}}}}return e},paymentIntents:(e=Za,t)=>{const{type:r,id:o}=t;switch(r){case Bt.SET_PAYMENT_INTENT:return Object.assign(Object.assign({},e),{[o]:Object.assign(Object.assign({},e[o]),{data:t.data})});case Bt.SET_ERROR_FOR_PAYMENT_INTENT:return Object.assign(Object.assign({},e),{[o]:Object.assign(Object.assign({},e[o]),{error:t.error})});default:return e}},authorizations:(e=en,t)=>{switch(t.type){case Xa.SET_AUTHORIZATION:{const{data:r}=t;return Object.assign(Object.assign({},e),{byId:Object.assign(Object.assign({},e.byId),{[r.payment_intent_id]:Object.assign(Object.assign({},e.byId[r.payment_intent_id]),r)})})}case Xa.SET_AUTHORIZATIONS:{const{data:r,query:o}=t;return Object.assign(Object.assign({},e),{[W(o)]:{data:r}})}case Xa.SET_ERROR_FOR_AUTHORIZATIONS:{const{error:r,query:o}=t;return Object.assign(Object.assign({},e),{[W(o)]:{error:r}})}case Xa.SET_AUTHORIZATIONS_SUMMARY:{const{data:r,query:o}=t;return Object.assign(Object.assign({},e),{summary:Object.assign(Object.assign({},e.summary),{[W(o)]:{data:r||{}}})})}case Xa.SET_ERROR_FOR_AUTHORIZATIONS_SUMMARY:{const{query:r,error:o}=t;return Object.assign(Object.assign({},e),{summary:Object.assign(Object.assign({},e.summary),{[W(r)]:{error:o||""}})})}case Xa.SET_IS_REQUESTING_AUTHORIZATION:{const{data:r}=t;return Object.assign(Object.assign({},e),{isRequesting:r})}}return e},files:(e=vn,t)=>{const{type:r,id:o}=t;switch(r){case On.SET_FILE:return Object.assign(Object.assign({},e),{[o]:Object.assign(Object.assign({},e[o]),{data:t.data})});case On.SET_ERROR_FOR_FILES:return Object.assign(Object.assign({},e),{[o]:Object.assign(Object.assign({},e[o]),{error:t.error})});default:return e}},pmPromotions:(e=Dn,t)=>{switch(t.type){case Pn.SET_PM_PROMOTIONS:return Object.assign(Object.assign({},e),{pmPromotions:t.data,pmPromotionsError:void 0});case Pn.SET_ERROR_FOR_PM_PROMOTIONS:return Object.assign(Object.assign({},e),{pmPromotions:void 0,pmPromotionsError:t.error})}return e}}),actions:{...t,...n,...c,..._,...h,...g,...w,...v,...A,...l,...D,...U,...x},controls:$.controls,selectors:{...e,...a,...i,...u,...m,...E,...R,...O,...I,...C,...P,...F,...k},resolvers:{...o,...s,...d,...p,...S,...T,...f,...b,...N,...y,...M,...L,...j}});const qn="/wc/v3/payments",Bn="wc/payments/multi-currency",$n={SET_CURRENCIES:"SET_CURRENCIES",SET_AVAILABLE_CURRENCIES:"SET_AVAILABLE_CURRENCIES",SET_ENABLED_CURRENCIES:"SET_ENABLED_CURRENCIES",SET_DEFAULT_CURRENCY:"SET_DEFAULT_CURRENCY",SET_CURRENCY_SETTINGS:"SET_CURRENCY_SETTINGS",SET_STORE_SETTINGS:"SET_STORE_SETTINGS"},Hn={},Vn=e=>e&&e||{},Gn=e=>Vn(e).currencies||{},Qn=e=>Gn(e).available||{},Wn=e=>Gn(e).enabled||{},Zn=e=>Gn(e).default||{},Kn=e=>Vn(e).currency_settings||{},Jn=e=>Vn(e).settings||{};function Xn(e){return{type:$n.SET_CURRENCIES,data:e}}function es(e){return{type:$n.SET_AVAILABLE_CURRENCIES,data:e}}function ts(e){return{type:$n.SET_ENABLED_CURRENCIES,data:e}}function rs(e){return{type:$n.SET_DEFAULT_CURRENCY,data:e}}function os(e,t){return{type:$n.SET_CURRENCY_SETTINGS,data:{code:e,data:t}}}function as(e){return{type:$n.SET_STORE_SETTINGS,data:e}}function*ns(e){const t=Object.keys((0,B.select)(Bn).getEnabledCurrencies()),r=e.filter((e=>!t.includes(e))),o=t.filter((t=>!e.includes(t)));try{const t=yield(0,$.apiFetch)({path:`${qn}/multi-currency/update-enabled-currencies`,method:"POST",data:{enabled:e}});yield Xn(t),yield(0,B.dispatch)("core/notices").createSuccessNotice((0,de.__)("Enabled currencies updated.","woocommerce-payments")),pe("wcpay_multi_currency_enabled_currencies_updated",{added_currencies:r,removed_currencies:o})}catch(e){yield(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error updating enabled currencies.","woocommerce-payments"))}}function*ss(e,t){try{const r=yield(0,$.apiFetch)({path:`${qn}/multi-currency/currencies/${e}`,method:"POST",data:t});yield os(e,r),yield(0,B.dispatch)("core/notices").createSuccessNotice((0,de.__)("Currency settings updated.","woocommerce-payments"))}catch(e){yield(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error updating currency settings.","woocommerce-payments"))}}function*is(e,t,r=!1){try{const o=yield(0,$.apiFetch)({path:`${qn}/multi-currency/update-settings`,method:"POST",data:{wcpay_multi_currency_enable_auto_currency:e?"yes":"no",wcpay_multi_currency_enable_storefront_switcher:t?"yes":"no"}});if(yield as(o),r)return;yield(0,B.dispatch)("core/notices").createSuccessNotice((0,de.__)("Store settings saved.","woocommerce-payments"))}catch(e){yield(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error saving store settings.","woocommerce-payments"))}}function cs(e,t){Eo()({path:`${qn}/settings/${e}`,method:"post",data:{value:t}}).catch((()=>{(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error saving option","woocommerce-payments"))}))}function*ds(){const e=`${qn}/multi-currency/currencies`;try{const t=yield(0,$.apiFetch)({path:e});yield Xn(t)}catch(e){yield(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error retrieving currencies.","woocommerce-payments"))}}function*us(e){const t=`${qn}/multi-currency/currencies/${e}`;try{const r=yield(0,$.apiFetch)({path:t});yield os(e,r)}catch(e){yield(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error retrieving single currency settings.","woocommerce-payments"))}}function*_s(){const e=`${qn}/multi-currency/get-settings`;try{const t=yield(0,$.apiFetch)({path:e});yield as(t)}catch(e){yield(0,B.dispatch)("core/notices").createErrorNotice((0,de.__)("Error retrieving store settings.","woocommerce-payments"))}}(0,B.registerStore)(Bn,{reducer:(e=Hn,{type:t,data:r=[]})=>{switch(t){case $n.SET_CURRENCIES:return{...e,currencies:r,available:r.available,enabled:r.enabled,default:r.default};case $n.SET_AVAILABLE_CURRENCIES:return{...e,currencies:{...e,available:r.available},available:r.available};case $n.SET_ENABLED_CURRENCIES:return{...e,currencies:{...e,enabled:r.enabled},enabled:r.enabled};case $n.SET_DEFAULT_CURRENCY:return{...e,currencies:{...e,default:r.default},default:r.default};case $n.SET_CURRENCY_SETTINGS:return{...e,currency_settings:{...e.currency_settings,[r.code]:r.data}};case $n.SET_STORE_SETTINGS:return{...e,settings:{enable_auto_currency:r.wcpay_multi_currency_enable_auto_currency,enable_storefront_switcher:r.wcpay_multi_currency_enable_storefront_switcher,site_theme:r.site_theme,date_format:r.date_format,time_format:r.time_format,store_url:r.store_url}}}return e},actions:{...z},controls:$.controls,selectors:{...Y},resolvers:{...q}});const ps=window.wp.components,ms=window.wp.blocks,ls=window.wp.blockEditor;var ys=r(4848);(0,ms.registerBlockType)("woocommerce-payments/multi-currency-switcher",{apiVersion:3,title:(0,de.__)("Currency Switcher Block","woocommerce-payments"),description:(0,de.__)("Let your customers switch between your enabled currencies","woocommerce-payments"),icon:(0,ys.jsxs)("svg",{xmlns:"http://www.w3.org/2000/svg",baseProfile:"tiny-ps",viewBox:"0 0 24 24",children:[(0,ys.jsx)("path",{d:"M13.5 8.1h.86c.34 1.81 1.54 2.9 3.2 2.9 1.58 0 2.64-.89 2.94-2.29h-1.11c-.23.72-.86 1.21-1.82 1.21-1 0-1.77-.67-2.06-1.82h2.78v-.65H15.4L15.38 7l.02-.46h2.89v-.65h-2.78c.3-1.13 1.06-1.81 2.05-1.81.94 0 1.63.59 1.84 1.38h1.1A2.86 2.86 0 0017.56 3c-1.65 0-2.85 1.09-3.19 2.89h-.87v.65h.78l-.02.46.02.45h-.78v.65zm-9 8.57v.78h1.8c.07.24.12.49.12.73 0 .87-.79 1.64-1.9 1.84V21h6.98v-1.01H6.57v-.07c.7-.33 1.25-.95 1.25-1.79 0-.23-.04-.45-.1-.68h2.72v-.78H7.43a3.6 3.6 0 01-.35-1.33c0-.83.77-1.33 2.12-1.33.74 0 1.51.13 1.85.29v-1.01A5.9 5.9 0 009.06 13c-2.15 0-3.44.87-3.44 2.28 0 .48.18.94.37 1.39H4.5z",className:"prefix__shp0"}),(0,ys.jsx)("path",{fillRule:"evenodd",d:"M7.63 10.22c-1.72-.09-3.08-.75-3.13-1.85h1.41c.13.54.64.93 1.72 1.01V7.37l-.26-.05c-1.73-.27-2.66-.8-2.66-1.73 0-1.04 1.22-1.72 2.92-1.8V3h.8v.79c1.69.09 2.91.78 2.96 1.82H9.98c-.08-.57-.69-.91-1.55-.99v1.93l.31.05c1.79.28 2.76.78 2.76 1.77 0 1.13-1.35 1.77-3.07 1.85V11h-.8v-.78zm0-3.8v-1.8c-.98.08-1.48.47-1.48.89 0 .42.4.73 1.48.91zm.8 1.07v1.9c1.15-.07 1.64-.44 1.64-.93 0-.48-.39-.76-1.64-.97z",className:"prefix__shp0"}),(0,ys.jsx)("path",{d:"M17.63 21v-1.61h1.97v-.67h-1.97v-.87h1.97v-.67h-1.63L20.5 13h-1.4l-2.07 3.63h-.05L14.92 13H13.5l2.53 4.18H14.4v.67h1.97v.87H14.4v.67h1.97V21h1.26z",opacity:.8})]}),category:"widgets",attributes:{symbol:{type:"boolean",default:!0},flag:{type:"boolean",default:!1},fontSize:{type:"integer",default:14},fontLineHeight:{type:"float",default:1.5},fontColor:{type:"string",default:"#000000"},border:{type:"boolean",default:!0},borderRadius:{type:"integer",default:3},borderColor:{type:"string",default:"#000000"},backgroundColor:{type:"string",default:"transparent"}},edit:e=>{const{attributes:{symbol:t,flag:r,fontSize:o,fontLineHeight:a,fontColor:n,border:s,borderRadius:i,borderColor:c,backgroundColor:d},setAttributes:u}=e,{isLoading:_}=(0,B.useSelect)((e=>{const{getCurrencies:t,isResolving:r}=e(Bn);return{currencies:t(),isLoading:r("getCurrencies",[])}}),[]),{enabledCurrencies:p}=(()=>{const e=(0,B.useSelect)((e=>{const{getEnabledCurrencies:t}=e(Bn);return t()})),{submitEnabledCurrenciesUpdate:t}=(0,B.useDispatch)(Bn);return{enabledCurrencies:e,submitEnabledCurrenciesUpdate:t}})(),m=p?Object.keys(p):[],l={div:{lineHeight:a},select:{fontSize:o,color:n,backgroundColor:d,borderWidth:s?"1px":"0px",borderColor:c,borderRadius:i}},y=(0,ls.useBlockProps)(),h=!window._wpemojiSettings||window._wpemojiSettings.supports?.flag;return(0,ys.jsxs)("div",{...y,children:[(0,ys.jsxs)(ls.InspectorControls,{children:[(0,ys.jsx)(ps.PanelBody,{title:(0,de.__)("Multi-Currency settings","woocommerce-payments"),children:(0,ys.jsx)(ps.ExternalLink,{href:"/wp-admin/admin.php?page=wc-settings&tab=wcpay_multi_currency",children:(0,de.__)("Adjust and edit your Multi-Currency settings","woocommerce-payments")})}),(0,ys.jsxs)(ps.PanelBody,{title:(0,de.__)("Layout","woocommerce-payments"),children:[(0,ys.jsx)(ps.CheckboxControl,{label:(0,de.__)("Display flags","woocommerce-payments"),checked:r,onChange:e=>{u({flag:e})},__nextHasNoMarginBottom:!0}),(0,ys.jsx)(ps.CheckboxControl,{label:(0,de.__)("Display currency symbols","woocommerce-payments"),checked:t,onChange:e=>{u({symbol:e})},__nextHasNoMarginBottom:!0}),(0,ys.jsx)(ps.CheckboxControl,{label:(0,de.__)("Border","woocommerce-payments"),checked:s,onChange:e=>{u({border:void 0!==e&&e})},__nextHasNoMarginBottom:!0}),(0,ys.jsx)(ps.RangeControl,{label:(0,de.__)("Border radius","woocommerce-payments"),value:i,onChange:e=>{u({borderRadius:void 0===e?3:e})},min:1,max:20,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0})]}),(0,ys.jsxs)(ps.PanelBody,{title:(0,de.__)("Typography","woocommerce-payments"),children:[(0,ys.jsx)(ps.RangeControl,{label:(0,de.__)("Size","woocommerce-payments"),value:o,onChange:e=>{u({fontSize:void 0===e?11:e})},min:6,max:48,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,ys.jsx)(ps.RangeControl,{label:(0,de.__)("Line height","woocommerce-payments"),value:a,onChange:e=>{u({fontLineHeight:void 0===e?"normal":e})},min:1,max:3,step:.1,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0})]}),(0,ys.jsxs)(ps.PanelBody,{title:(0,de.__)("Color settings","woocommerce-payments"),children:[(0,ys.jsx)(ls.ColorPaletteControl,{onChange:e=>{u({fontColor:void 0===e?"#000000":e})},value:n,label:(0,de.__)("Text","woocommerce-payments")}),(0,ys.jsx)(ls.ColorPaletteControl,{onChange:e=>{u({backgroundColor:void 0===e?"transparent":e})},value:d,label:(0,de.__)("Background","woocommerce-payments")}),(0,ys.jsx)(ls.ColorPaletteControl,{onChange:e=>{u({borderColor:void 0===e?"#000000":e})},value:c,label:(0,de.__)("Border","woocommerce-payments")})]})]},"setting"),(0,ys.jsx)("div",{className:"currency-switcher-holder",style:l.div,children:(0,ys.jsxs)("select",{name:"currency",disabled:_,style:l.select,children:[_&&(0,ys.jsx)("option",{children:(0,de.__)("Loading…","woocommerce-payments")}),!_&&p&&m.map((e=>(0,ys.jsxs)("option",{value:p[e].code,children:[h&&r?p[e].flag+" ":"",t&&p[e].symbol!==p[e].code?p[e].symbol+" ":"",p[e].code]},p[e].id)))]})})]})},save:()=>null})})()})(); dist/wc-payments-settings-spotlight.js.LICENSE.txt 0000777 00000000666 15251156605 0016111 0 ustar 00 /*! @license DOMPurify 2.5.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.5.6/LICENSE */ /** * @license React * react-jsx-runtime.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ dist/success.js 0000777 00000040665 15251156605 0007545 0 ustar 00 (()=>{var t={};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),(()=>{var r;t.g.importScripts&&(r=t.g.location+"");var e=t.g.document;if(!r&&e&&(e.currentScript&&(r=e.currentScript.src),!r)){var n=e.getElementsByTagName("script");if(n.length)for(var a=n.length-1;a>-1&&(!r||!/^http(s?):/.test(r));)r=n[a--].src}if(!r)throw new Error("Automatic publicPath is not supported in this browser");r=r.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),t.p=r})(),t.p=window.wcpayAssets.url,(()=>{"use strict";function t(r){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(r)}var r=/^\s+/,e=/\s+$/;function n(a,i){if(i=i||{},(a=a||"")instanceof n)return a;if(!(this instanceof n))return new n(a,i);var o=function(n){var a,i,o,s={r:0,g:0,b:0},h=1,f=null,u=null,l=null,c=!1,d=!1;return"string"==typeof n&&(n=function(t){t=t.replace(r,"").replace(e,"").toLowerCase();var n,a=!1;if(v[t])t=v[t],a=!0;else if("transparent"==t)return{r:0,g:0,b:0,a:0,format:"name"};return(n=q.rgb.exec(t))?{r:n[1],g:n[2],b:n[3]}:(n=q.rgba.exec(t))?{r:n[1],g:n[2],b:n[3],a:n[4]}:(n=q.hsl.exec(t))?{h:n[1],s:n[2],l:n[3]}:(n=q.hsla.exec(t))?{h:n[1],s:n[2],l:n[3],a:n[4]}:(n=q.hsv.exec(t))?{h:n[1],s:n[2],v:n[3]}:(n=q.hsva.exec(t))?{h:n[1],s:n[2],v:n[3],a:n[4]}:(n=q.hex8.exec(t))?{r:k(n[1]),g:k(n[2]),b:k(n[3]),a:F(n[4]),format:a?"name":"hex8"}:(n=q.hex6.exec(t))?{r:k(n[1]),g:k(n[2]),b:k(n[3]),format:a?"name":"hex"}:(n=q.hex4.exec(t))?{r:k(n[1]+""+n[1]),g:k(n[2]+""+n[2]),b:k(n[3]+""+n[3]),a:F(n[4]+""+n[4]),format:a?"name":"hex8"}:!!(n=q.hex3.exec(t))&&{r:k(n[1]+""+n[1]),g:k(n[2]+""+n[2]),b:k(n[3]+""+n[3]),format:a?"name":"hex"}}(n)),"object"==t(n)&&(P(n.r)&&P(n.g)&&P(n.b)?(a=n.r,i=n.g,o=n.b,s={r:255*w(a,255),g:255*w(i,255),b:255*w(o,255)},c=!0,d="%"===String(n.r).substr(-1)?"prgb":"rgb"):P(n.h)&&P(n.s)&&P(n.v)?(f=R(n.s),u=R(n.v),s=function(t,r,e){t=6*w(t,360),r=w(r,100),e=w(e,100);var n=Math.floor(t),a=t-n,i=e*(1-r),o=e*(1-a*r),s=e*(1-(1-a)*r),h=n%6;return{r:255*[e,o,i,i,s,e][h],g:255*[s,e,e,o,i,i][h],b:255*[i,i,s,e,e,o][h]}}(n.h,f,u),c=!0,d="hsv"):P(n.h)&&P(n.s)&&P(n.l)&&(f=R(n.s),l=R(n.l),s=function(t,r,e){var n,a,i;function o(t,r,e){return e<0&&(e+=1),e>1&&(e-=1),e<1/6?t+6*(r-t)*e:e<.5?r:e<2/3?t+(r-t)*(2/3-e)*6:t}if(t=w(t,360),r=w(r,100),e=w(e,100),0===r)n=a=i=e;else{var s=e<.5?e*(1+r):e+r-e*r,h=2*e-s;n=o(h,s,t+1/3),a=o(h,s,t),i=o(h,s,t-1/3)}return{r:255*n,g:255*a,b:255*i}}(n.h,f,l),c=!0,d="hsl"),n.hasOwnProperty("a")&&(h=n.a)),h=A(h),{ok:c,format:n.format||d,r:Math.min(255,Math.max(s.r,0)),g:Math.min(255,Math.max(s.g,0)),b:Math.min(255,Math.max(s.b,0)),a:h}}(a);this._originalInput=a,this._r=o.r,this._g=o.g,this._b=o.b,this._a=o.a,this._roundA=Math.round(100*this._a)/100,this._format=i.format||o.format,this._gradientType=i.gradientType,this._r<1&&(this._r=Math.round(this._r)),this._g<1&&(this._g=Math.round(this._g)),this._b<1&&(this._b=Math.round(this._b)),this._ok=o.ok}function a(t,r,e){t=w(t,255),r=w(r,255),e=w(e,255);var n,a,i=Math.max(t,r,e),o=Math.min(t,r,e),s=(i+o)/2;if(i==o)n=a=0;else{var h=i-o;switch(a=s>.5?h/(2-i-o):h/(i+o),i){case t:n=(r-e)/h+(r<e?6:0);break;case r:n=(e-t)/h+2;break;case e:n=(t-r)/h+4}n/=6}return{h:n,s:a,l:s}}function i(t,r,e){t=w(t,255),r=w(r,255),e=w(e,255);var n,a,i=Math.max(t,r,e),o=Math.min(t,r,e),s=i,h=i-o;if(a=0===i?0:h/i,i==o)n=0;else{switch(i){case t:n=(r-e)/h+(r<e?6:0);break;case r:n=(e-t)/h+2;break;case e:n=(t-r)/h+4}n/=6}return{h:n,s:a,v:s}}function o(t,r,e,n){var a=[S(Math.round(t).toString(16)),S(Math.round(r).toString(16)),S(Math.round(e).toString(16))];return n&&a[0].charAt(0)==a[0].charAt(1)&&a[1].charAt(0)==a[1].charAt(1)&&a[2].charAt(0)==a[2].charAt(1)?a[0].charAt(0)+a[1].charAt(0)+a[2].charAt(0):a.join("")}function s(t,r,e,n){return[S(H(n)),S(Math.round(t).toString(16)),S(Math.round(r).toString(16)),S(Math.round(e).toString(16))].join("")}function h(t,r){r=0===r?0:r||10;var e=n(t).toHsl();return e.s-=r/100,e.s=x(e.s),n(e)}function f(t,r){r=0===r?0:r||10;var e=n(t).toHsl();return e.s+=r/100,e.s=x(e.s),n(e)}function u(t){return n(t).desaturate(100)}function l(t,r){r=0===r?0:r||10;var e=n(t).toHsl();return e.l+=r/100,e.l=x(e.l),n(e)}function c(t,r){r=0===r?0:r||10;var e=n(t).toRgb();return e.r=Math.max(0,Math.min(255,e.r-Math.round(-r/100*255))),e.g=Math.max(0,Math.min(255,e.g-Math.round(-r/100*255))),e.b=Math.max(0,Math.min(255,e.b-Math.round(-r/100*255))),n(e)}function d(t,r){r=0===r?0:r||10;var e=n(t).toHsl();return e.l-=r/100,e.l=x(e.l),n(e)}function g(t,r){var e=n(t).toHsl(),a=(e.h+r)%360;return e.h=a<0?360+a:a,n(e)}function b(t){var r=n(t).toHsl();return r.h=(r.h+180)%360,n(r)}function p(t,r){if(isNaN(r)||r<=0)throw new Error("Argument to polyad must be a positive number");for(var e=n(t).toHsl(),a=[n(t)],i=360/r,o=1;o<r;o++)a.push(n({h:(e.h+o*i)%360,s:e.s,l:e.l}));return a}function m(t){var r=n(t).toHsl(),e=r.h;return[n(t),n({h:(e+72)%360,s:r.s,l:r.l}),n({h:(e+216)%360,s:r.s,l:r.l})]}function _(t,r,e){r=r||6,e=e||30;var a=n(t).toHsl(),i=360/e,o=[n(t)];for(a.h=(a.h-(i*r>>1)+720)%360;--r;)a.h=(a.h+i)%360,o.push(n(a));return o}function y(t,r){r=r||6;for(var e=n(t).toHsv(),a=e.h,i=e.s,o=e.v,s=[],h=1/r;r--;)s.push(n({h:a,s:i,v:o})),o=(o+h)%1;return s}n.prototype={isDark:function(){return this.getBrightness()<128},isLight:function(){return!this.isDark()},isValid:function(){return this._ok},getOriginalInput:function(){return this._originalInput},getFormat:function(){return this._format},getAlpha:function(){return this._a},getBrightness:function(){var t=this.toRgb();return(299*t.r+587*t.g+114*t.b)/1e3},getLuminance:function(){var t,r,e,n=this.toRgb();return t=n.r/255,r=n.g/255,e=n.b/255,.2126*(t<=.03928?t/12.92:Math.pow((t+.055)/1.055,2.4))+.7152*(r<=.03928?r/12.92:Math.pow((r+.055)/1.055,2.4))+.0722*(e<=.03928?e/12.92:Math.pow((e+.055)/1.055,2.4))},setAlpha:function(t){return this._a=A(t),this._roundA=Math.round(100*this._a)/100,this},toHsv:function(){var t=i(this._r,this._g,this._b);return{h:360*t.h,s:t.s,v:t.v,a:this._a}},toHsvString:function(){var t=i(this._r,this._g,this._b),r=Math.round(360*t.h),e=Math.round(100*t.s),n=Math.round(100*t.v);return 1==this._a?"hsv("+r+", "+e+"%, "+n+"%)":"hsva("+r+", "+e+"%, "+n+"%, "+this._roundA+")"},toHsl:function(){var t=a(this._r,this._g,this._b);return{h:360*t.h,s:t.s,l:t.l,a:this._a}},toHslString:function(){var t=a(this._r,this._g,this._b),r=Math.round(360*t.h),e=Math.round(100*t.s),n=Math.round(100*t.l);return 1==this._a?"hsl("+r+", "+e+"%, "+n+"%)":"hsla("+r+", "+e+"%, "+n+"%, "+this._roundA+")"},toHex:function(t){return o(this._r,this._g,this._b,t)},toHexString:function(t){return"#"+this.toHex(t)},toHex8:function(t){return function(t,r,e,n,a){var i=[S(Math.round(t).toString(16)),S(Math.round(r).toString(16)),S(Math.round(e).toString(16)),S(H(n))];return a&&i[0].charAt(0)==i[0].charAt(1)&&i[1].charAt(0)==i[1].charAt(1)&&i[2].charAt(0)==i[2].charAt(1)&&i[3].charAt(0)==i[3].charAt(1)?i[0].charAt(0)+i[1].charAt(0)+i[2].charAt(0)+i[3].charAt(0):i.join("")}(this._r,this._g,this._b,this._a,t)},toHex8String:function(t){return"#"+this.toHex8(t)},toRgb:function(){return{r:Math.round(this._r),g:Math.round(this._g),b:Math.round(this._b),a:this._a}},toRgbString:function(){return 1==this._a?"rgb("+Math.round(this._r)+", "+Math.round(this._g)+", "+Math.round(this._b)+")":"rgba("+Math.round(this._r)+", "+Math.round(this._g)+", "+Math.round(this._b)+", "+this._roundA+")"},toPercentageRgb:function(){return{r:Math.round(100*w(this._r,255))+"%",g:Math.round(100*w(this._g,255))+"%",b:Math.round(100*w(this._b,255))+"%",a:this._a}},toPercentageRgbString:function(){return 1==this._a?"rgb("+Math.round(100*w(this._r,255))+"%, "+Math.round(100*w(this._g,255))+"%, "+Math.round(100*w(this._b,255))+"%)":"rgba("+Math.round(100*w(this._r,255))+"%, "+Math.round(100*w(this._g,255))+"%, "+Math.round(100*w(this._b,255))+"%, "+this._roundA+")"},toName:function(){return 0===this._a?"transparent":!(this._a<1)&&(M[o(this._r,this._g,this._b,!0)]||!1)},toFilter:function(t){var r="#"+s(this._r,this._g,this._b,this._a),e=r,a=this._gradientType?"GradientType = 1, ":"";if(t){var i=n(t);e="#"+s(i._r,i._g,i._b,i._a)}return"progid:DXImageTransform.Microsoft.gradient("+a+"startColorstr="+r+",endColorstr="+e+")"},toString:function(t){var r=!!t;t=t||this._format;var e=!1,n=this._a<1&&this._a>=0;return r||!n||"hex"!==t&&"hex6"!==t&&"hex3"!==t&&"hex4"!==t&&"hex8"!==t&&"name"!==t?("rgb"===t&&(e=this.toRgbString()),"prgb"===t&&(e=this.toPercentageRgbString()),"hex"!==t&&"hex6"!==t||(e=this.toHexString()),"hex3"===t&&(e=this.toHexString(!0)),"hex4"===t&&(e=this.toHex8String(!0)),"hex8"===t&&(e=this.toHex8String()),"name"===t&&(e=this.toName()),"hsl"===t&&(e=this.toHslString()),"hsv"===t&&(e=this.toHsvString()),e||this.toHexString()):"name"===t&&0===this._a?this.toName():this.toRgbString()},clone:function(){return n(this.toString())},_applyModification:function(t,r){var e=t.apply(null,[this].concat([].slice.call(r)));return this._r=e._r,this._g=e._g,this._b=e._b,this.setAlpha(e._a),this},lighten:function(){return this._applyModification(l,arguments)},brighten:function(){return this._applyModification(c,arguments)},darken:function(){return this._applyModification(d,arguments)},desaturate:function(){return this._applyModification(h,arguments)},saturate:function(){return this._applyModification(f,arguments)},greyscale:function(){return this._applyModification(u,arguments)},spin:function(){return this._applyModification(g,arguments)},_applyCombination:function(t,r){return t.apply(null,[this].concat([].slice.call(r)))},analogous:function(){return this._applyCombination(_,arguments)},complement:function(){return this._applyCombination(b,arguments)},monochromatic:function(){return this._applyCombination(y,arguments)},splitcomplement:function(){return this._applyCombination(m,arguments)},triad:function(){return this._applyCombination(p,[3])},tetrad:function(){return this._applyCombination(p,[4])}},n.fromRatio=function(r,e){if("object"==t(r)){var a={};for(var i in r)r.hasOwnProperty(i)&&(a[i]="a"===i?r[i]:R(r[i]));r=a}return n(r,e)},n.equals=function(t,r){return!(!t||!r)&&n(t).toRgbString()==n(r).toRgbString()},n.random=function(){return n.fromRatio({r:Math.random(),g:Math.random(),b:Math.random()})},n.mix=function(t,r,e){e=0===e?0:e||50;var a=n(t).toRgb(),i=n(r).toRgb(),o=e/100;return n({r:(i.r-a.r)*o+a.r,g:(i.g-a.g)*o+a.g,b:(i.b-a.b)*o+a.b,a:(i.a-a.a)*o+a.a})},n.readability=function(t,r){var e=n(t),a=n(r);return(Math.max(e.getLuminance(),a.getLuminance())+.05)/(Math.min(e.getLuminance(),a.getLuminance())+.05)},n.isReadable=function(t,r,e){var a,i,o,s,h,f=n.readability(t,r);switch(i=!1,(o=e,"AA"!==(s=((o=o||{level:"AA",size:"small"}).level||"AA").toUpperCase())&&"AAA"!==s&&(s="AA"),"small"!==(h=(o.size||"small").toLowerCase())&&"large"!==h&&(h="small"),a={level:s,size:h}).level+a.size){case"AAsmall":case"AAAlarge":i=f>=4.5;break;case"AAlarge":i=f>=3;break;case"AAAsmall":i=f>=7}return i},n.mostReadable=function(t,r,e){var a,i,o,s,h=null,f=0;i=(e=e||{}).includeFallbackColors,o=e.level,s=e.size;for(var u=0;u<r.length;u++)(a=n.readability(t,r[u]))>f&&(f=a,h=n(r[u]));return n.isReadable(t,h,{level:o,size:s})||!i?h:(e.includeFallbackColors=!1,n.mostReadable(t,["#fff","#000"],e))};var v=n.names={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"0ff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000",blanchedalmond:"ffebcd",blue:"00f",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",burntsienna:"ea7e5d",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"0ff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkgrey:"a9a9a9",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkslategrey:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dimgrey:"696969",dodgerblue:"1e90ff",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"f0f",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",grey:"808080",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgray:"d3d3d3",lightgreen:"90ee90",lightgrey:"d3d3d3",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslategray:"789",lightslategrey:"789",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"0f0",limegreen:"32cd32",linen:"faf0e6",magenta:"f0f",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370db",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"db7093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",rebeccapurple:"663399",red:"f00",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",slategrey:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",wheat:"f5deb3",white:"fff",whitesmoke:"f5f5f5",yellow:"ff0",yellowgreen:"9acd32"},M=n.hexNames=function(t){var r={};for(var e in t)t.hasOwnProperty(e)&&(r[t[e]]=e);return r}(v);function A(t){return t=parseFloat(t),(isNaN(t)||t<0||t>1)&&(t=1),t}function w(t,r){(function(t){return"string"==typeof t&&-1!=t.indexOf(".")&&1===parseFloat(t)})(t)&&(t="100%");var e=function(t){return"string"==typeof t&&-1!=t.indexOf("%")}(t);return t=Math.min(r,Math.max(0,parseFloat(t))),e&&(t=parseInt(t*r,10)/100),Math.abs(t-r)<1e-6?1:t%r/parseFloat(r)}function x(t){return Math.min(1,Math.max(0,t))}function k(t){return parseInt(t,16)}function S(t){return 1==t.length?"0"+t:""+t}function R(t){return t<=1&&(t=100*t+"%"),t}function H(t){return Math.round(255*parseFloat(t)).toString(16)}function F(t){return k(t)/255}var C,E,L,q=(E="[\\s|\\(]+("+(C="(?:[-\\+]?\\d*\\.\\d+%?)|(?:[-\\+]?\\d+%?)")+")[,|\\s]+("+C+")[,|\\s]+("+C+")\\s*\\)?",L="[\\s|\\(]+("+C+")[,|\\s]+("+C+")[,|\\s]+("+C+")[,|\\s]+("+C+")\\s*\\)?",{CSS_UNIT:new RegExp(C),rgb:new RegExp("rgb"+E),rgba:new RegExp("rgba"+L),hsl:new RegExp("hsl"+E),hsla:new RegExp("hsla"+L),hsv:new RegExp("hsv"+E),hsva:new RegExp("hsva"+L),hex3:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex6:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,hex4:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex8:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/});function P(t){return!!q.CSS_UNIT.exec(t)}document.addEventListener("DOMContentLoaded",(()=>{const t=document.getElementById("wc-payment-gateway-multibanco-instructions-container");if(t){const r=window.getComputedStyle(t).color,e=(t=>{let r=t,e=window.getComputedStyle(r).backgroundColor;for(;r;){if(n(e).getAlpha()>0)return e;if(r=r.parentElement,!r)return"rgb(255, 255, 255)";e=window.getComputedStyle(r).backgroundColor}return"rgb(255, 255, 255)"})(t.parentElement),a=(t,r)=>{const e=n(t);return e.setAlpha(r),e.toRgbString()};t.style.setProperty("--woopayments-multibanco-text-color",r),t.style.setProperty("--woopayments-multibanco-bg-color",a(r,.06)),t.style.setProperty("--woopayments-multibanco-border-color",a(r,.16)),t.style.setProperty("--woopayments-multibanco-card-bg-color",e),t.querySelectorAll(".copy-btn").forEach((t=>{t.addEventListener("click",(()=>{const r=t.dataset.copyValue;r&&navigator.clipboard.writeText(r).then((()=>{t.classList.add("copied"),setTimeout((()=>{t.classList.remove("copied")}),2e3)})).catch((()=>{prompt("Failed to copy text. Please copy it manually:",r)}))}))}));const i=t.querySelector(".print-btn");i&&i.addEventListener("click",(()=>{window.print()}))}}))})()})(); dist/woopay-express-button.js 0000777 00000205536 15251156605 0012413 0 ustar 00 /*! For license information please see woopay-express-button.js.LICENSE.txt */ (()=>{var e={5338:(e,t,o)=>{"use strict";var r=o(5795);t.H=r.createRoot,r.hydrateRoot},1020:(e,t,o)=>{"use strict";var r=o(1609),n=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,s={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,o){var r,c={},l=null,u=null;for(r in void 0!==o&&(l=""+o),void 0!==t.key&&(l=""+t.key),void 0!==t.ref&&(u=t.ref),t)a.call(t,r)&&!s.hasOwnProperty(r)&&(c[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===c[r]&&(c[r]=t[r]);return{$$typeof:n,type:e,key:l,ref:u,props:c,_owner:i.current}}t.jsx=c,t.jsxs=c},4848:(e,t,o)=>{"use strict";e.exports=o(1020)},6255:(e,t,o)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){var o,r;(0,n.default)(e),"object"===a(t)?(o=t.min||0,r=t.max):(o=arguments[1],r=arguments[2]);var i=encodeURI(e).split(/%..|./).length-1;return i>=o&&(void 0===r||i<=r)};var r,n=(r=o(3399))&&r.__esModule?r:{default:r};function a(e){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a(e)}e.exports=t.default,e.exports.default=t.default},9517:(e,t,o)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){if((0,r.default)(e),(t=(0,s.default)(t,l)).require_display_name||t.allow_display_name){var o=e.match(u);if(o){var c=o[1];if(e=e.replace(c,"").replace(/(^<|>$)/g,""),c.endsWith(" ")&&(c=c.slice(0,-1)),!function(e){var t=e.replace(/^"(.+)"$/,"$1");if(!t.trim())return!1;if(/[\.";<>]/.test(t)){if(t===e)return!1;if(t.split('"').length!==t.split('\\"').length)return!1}return!0}(c))return!1}else if(t.require_display_name)return!1}if(!t.ignore_max_length&&e.length>g)return!1;var b=e.split("@"),_=b.pop(),y=_.toLowerCase();if(t.host_blacklist.includes(y))return!1;if(t.host_whitelist.length>0&&!t.host_whitelist.includes(y))return!1;var w=b.join("@");if(t.domain_specific_validation&&("gmail.com"===y||"googlemail.com"===y)){var v=(w=w.toLowerCase()).split("+")[0];if(!(0,n.default)(v.replace(/\./g,""),{min:6,max:30}))return!1;for(var k=v.split("."),S=0;S<k.length;S++)if(!h.test(k[S]))return!1}if(!(!1!==t.ignore_max_length||(0,n.default)(w,{max:64})&&(0,n.default)(_,{max:254})))return!1;if(!(0,a.default)(_,{require_tld:t.require_tld,ignore_max_length:t.ignore_max_length,allow_underscores:t.allow_underscores})){if(!t.allow_ip_domain)return!1;if(!(0,i.default)(_)){if(!_.startsWith("[")||!_.endsWith("]"))return!1;var x=_.slice(1,-1);if(0===x.length||!(0,i.default)(x))return!1}}if('"'===w[0])return w=w.slice(1,w.length-1),t.allow_utf8_local_part?m.test(w):p.test(w);for(var F=t.allow_utf8_local_part?f:d,C=w.split("."),P=0;P<C.length;P++)if(!F.test(C[P]))return!1;return!t.blacklisted_chars||-1===w.search(new RegExp("[".concat(t.blacklisted_chars,"]+"),"g"))};var r=c(o(3399)),n=c(o(6255)),a=c(o(7658)),i=c(o(5372)),s=c(o(3610));function c(e){return e&&e.__esModule?e:{default:e}}var l={allow_display_name:!1,allow_underscores:!1,require_display_name:!1,allow_utf8_local_part:!0,require_tld:!0,blacklisted_chars:"",ignore_max_length:!1,host_blacklist:[],host_whitelist:[]},u=/^([^\x00-\x1F\x7F-\x9F\cX]+)</i,d=/^[a-z\d!#\$%&'\*\+\-\/=\?\^_`{\|}~]+$/i,h=/^[a-z\d]+$/,p=/^([\s\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e]|(\\[\x01-\x09\x0b\x0c\x0d-\x7f]))*$/i,f=/^[a-z\d!#\$%&'\*\+\-\/=\?\^_`{\|}~\u00A1-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+$/i,m=/^([\s\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|(\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*$/i,g=254;e.exports=t.default,e.exports.default=t.default},7658:(e,t,o)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){(0,r.default)(e),(t=(0,n.default)(t,i)).allow_trailing_dot&&"."===e[e.length-1]&&(e=e.substring(0,e.length-1)),!0===t.allow_wildcard&&0===e.indexOf("*.")&&(e=e.substring(2));var o=e.split("."),a=o[o.length-1];if(t.require_tld){if(o.length<2)return!1;if(!t.allow_numeric_tld&&!/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i.test(a))return!1;if(/\s/.test(a))return!1}return!(!t.allow_numeric_tld&&/^\d+$/.test(a))&&o.every((function(e){return!(e.length>63&&!t.ignore_max_length||!/^[a-z_\u00a1-\uffff0-9-]+$/i.test(e)||/[\uff01-\uff5e]/.test(e)||/^-|-$/.test(e)||!t.allow_underscores&&/_/.test(e))}))};var r=a(o(3399)),n=a(o(3610));function a(e){return e&&e.__esModule?e:{default:e}}var i={require_tld:!0,allow_underscores:!1,allow_trailing_dot:!1,allow_numeric_tld:!1,allow_wildcard:!1,ignore_max_length:!1};e.exports=t.default,e.exports.default=t.default},5372:(e,t,o)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function e(t){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return(0,n.default)(t),(o=String(o))?"4"===o?s.test(t):"6"===o&&l.test(t):e(t,4)||e(t,6)};var r,n=(r=o(3399))&&r.__esModule?r:{default:r},a="(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])",i="(".concat(a,"[.]){3}").concat(a),s=new RegExp("^".concat(i,"$")),c="(?:[0-9a-fA-F]{1,4})",l=new RegExp("^("+"(?:".concat(c,":){7}(?:").concat(c,"|:)|")+"(?:".concat(c,":){6}(?:").concat(i,"|:").concat(c,"|:)|")+"(?:".concat(c,":){5}(?::").concat(i,"|(:").concat(c,"){1,2}|:)|")+"(?:".concat(c,":){4}(?:(:").concat(c,"){0,1}:").concat(i,"|(:").concat(c,"){1,3}|:)|")+"(?:".concat(c,":){3}(?:(:").concat(c,"){0,2}:").concat(i,"|(:").concat(c,"){1,4}|:)|")+"(?:".concat(c,":){2}(?:(:").concat(c,"){0,3}:").concat(i,"|(:").concat(c,"){1,5}|:)|")+"(?:".concat(c,":){1}(?:(:").concat(c,"){0,4}:").concat(i,"|(:").concat(c,"){1,6}|:)|")+"(?::((?::".concat(c,"){0,5}:").concat(i,"|(?::").concat(c,"){1,7}|:))")+")(%[0-9a-zA-Z-.:]{1,})?$");e.exports=t.default,e.exports.default=t.default},3399:(e,t)=>{"use strict";function o(e){return o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},o(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){if(!("string"==typeof e||e instanceof String)){var t=o(e);throw null===e?t="null":"object"===t&&(t=e.constructor.name),new TypeError("Expected a string but received a ".concat(t))}},e.exports=t.default,e.exports.default=t.default},3610:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0;for(var o in t)void 0===e[o]&&(e[o]=t[o]);return e},e.exports=t.default,e.exports.default=t.default},1609:e=>{"use strict";e.exports=window.React},5795:e=>{"use strict";e.exports=window.ReactDOM}},t={};function o(r){var n=t[r];if(void 0!==n)return n.exports;var a=t[r]={exports:{}};return e[r](a,a.exports,o),a.exports}o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var t=o.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var n=r.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=r[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),o.p=window.wcpayAssets.url,(()=>{"use strict";var e=o(5338);const t=e=>"undefined"!=typeof wcpayConfig&&void 0!==wcpayConfig[e]?wcpayConfig[e]:r(e),r=e=>{let t=null;if("undefined"!=typeof wcpay_upe_config)t=wcpay_upe_config;else{if("object"!=typeof wc||void 0===wc.wcSettings)return null;t=wc.wcSettings.getSetting("woocommerce_payments_data")||{}}return t[e]||null},n=window.wp.i18n;var a=o(1609);function i(e){var t,o,r="";if("string"==typeof e||"number"==typeof e)r+=e;else if("object"==typeof e)if(Array.isArray(e)){var n=e.length;for(t=0;t<n;t++)e[t]&&(o=i(e[t]))&&(r&&(r+=" "),r+=o)}else for(o in e)e[o]&&(r&&(r+=" "),r+=o);return r}const s=function(){for(var e,t,o=0,r="",n=arguments.length;o<n;o++)(e=arguments[o])&&(t=i(e))&&(r&&(r+=" "),r+=t);return r};var c=o(4848);const l=e=>(0,c.jsx)("svg",{viewBox:"0 0 109 28",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:(0,c.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M69.496 5.785v16.42h2.766v-6.179h4.37c1.104 0 2.059-.23 2.865-.689.807-.46 1.424-1.075 1.852-1.846.428-.788.642-1.65.642-2.585 0-.936-.214-1.79-.642-2.56a4.714 4.714 0 0 0-1.852-1.872c-.79-.46-1.745-.69-2.864-.69h-7.137Zm2.766 7.804h4c.56 0 1.054-.107 1.482-.32.445-.23.79-.55 1.037-.96.247-.41.37-.878.37-1.403 0-.526-.123-.993-.37-1.404a2.386 2.386 0 0 0-1.037-.935c-.428-.23-.922-.345-1.482-.345h-4v5.367Zm15.693 8.912c-1.02 0-1.934-.246-2.74-.739-.808-.508-1.441-1.23-1.902-2.166-.445-.936-.667-2.043-.667-3.323 0-1.264.222-2.364.667-3.3.46-.951 1.094-1.682 1.901-2.19.807-.51 1.72-.764 2.741-.764.823 0 1.597.206 2.321.616a4.761 4.761 0 0 1 1.556 1.352v-1.672h2.47v11.89h-2.47v-1.667a5.231 5.231 0 0 1-1.556 1.372 4.695 4.695 0 0 1-2.32.591Zm2.692-9.675c.53.311.924.673 1.185 1.084v4.757c-.26.423-.656.79-1.185 1.101-.642.361-1.3.542-1.976.542-1.053 0-1.893-.37-2.519-1.108-.609-.755-.913-1.731-.913-2.93 0-.787.132-1.485.395-2.092a3.327 3.327 0 0 1 1.21-1.428c.527-.345 1.136-.517 1.827-.517.675 0 1.334.197 1.976.59Zm6.422 11.817c.115.066.271.115.469.148.197.032.395.049.592.049.395 0 .725-.082.988-.246.264-.164.478-.435.642-.813l.625-1.442-4.897-12.024h2.642l3.556 9.035 3.556-9.035h2.667l-5.803 14.106c0 .017-.008.025-.024.025v.05c-.264.607-.585 1.099-.964 1.476a3.337 3.337 0 0 1-1.284.813c-.477.164-1.02.246-1.63.246a6.98 6.98 0 0 1-1.53-.172l.395-2.216ZM39.45 5.512c-4.856 0-8.575 3.614-8.575 8.502 0 4.888 3.743 8.478 8.575 8.478 4.832 0 8.527-3.614 8.551-8.478 0-4.888-3.719-8.502-8.551-8.502Zm0 11.76c-1.824 0-3.08-1.369-3.08-3.258 0-1.89 1.256-3.283 3.08-3.283s3.08 1.394 3.08 3.283-1.233 3.259-3.08 3.259Zm-30.463 5.22c1.919 0 3.458-.945 4.619-3.117l2.582-4.818v4.085c0 2.41 1.563 3.85 3.98 3.85 1.894 0 3.292-.827 4.642-3.117l5.946-10.013c1.302-2.196.378-3.85-2.488-3.85-1.54 0-2.534.496-3.434 2.173l-4.098 7.675V8.535c0-2.03-.971-3.022-2.772-3.022-1.42 0-2.558.614-3.434 2.314l-3.861 7.533V8.606c0-2.172-.9-3.093-3.08-3.093H3.136c-1.682 0-2.534.779-2.534 2.22 0 1.44.9 2.266 2.534 2.266H4.96v8.62c0 2.432 1.634 3.873 4.027 3.873Zm40.221-8.478c0-4.888 3.719-8.502 8.551-8.502 4.832 0 8.551 3.637 8.551 8.502 0 4.864-3.719 8.478-8.55 8.478-4.833 0-8.552-3.59-8.552-8.478Zm5.495 0c0 1.889 1.208 3.259 3.056 3.259 1.824 0 3.08-1.37 3.08-3.26 0-1.888-1.256-3.282-3.08-3.282s-3.056 1.394-3.056 3.283Z",fill:"#fff",...e})}),u=e=>(0,c.jsxs)("svg",{viewBox:"0 0 100 24",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[(0,c.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M63.963 4.435v15.121h2.547v-5.69h4.025c1.016 0 1.895-.212 2.638-.635a4.384 4.384 0 0 0 1.705-1.7 4.906 4.906 0 0 0 .591-2.38c0-.862-.197-1.648-.59-2.358a4.341 4.341 0 0 0-1.706-1.723c-.728-.423-1.607-.635-2.638-.635h-6.572Zm2.547 7.187h3.684c.515 0 .97-.099 1.364-.295.41-.212.728-.506.955-.884.228-.378.341-.809.341-1.292 0-.484-.113-.915-.34-1.293a2.197 2.197 0 0 0-.956-.861c-.394-.212-.849-.318-1.364-.318H66.51v4.943Zm14.451 8.206c-.94 0-1.781-.226-2.524-.68-.743-.468-1.326-1.133-1.75-1.995-.41-.861-.615-1.881-.615-3.06 0-1.164.205-2.177.614-3.038.425-.877 1.008-1.55 1.751-2.018.743-.468 1.584-.703 2.524-.703.758 0 1.47.19 2.138.567a4.384 4.384 0 0 1 1.432 1.245v-1.54h2.274v10.95h-2.274V18.02c-.393.517-.87.938-1.432 1.264a4.323 4.323 0 0 1-2.138.544Zm2.479-8.91c.487.287.851.62 1.091 1v4.38c-.24.39-.604.727-1.091 1.014-.591.332-1.198.499-1.82.499-.97 0-1.743-.34-2.319-1.02-.56-.696-.841-1.595-.841-2.698 0-.726.121-1.368.364-1.927.257-.56.629-.998 1.114-1.315.485-.318 1.046-.476 1.683-.476.621 0 1.228.181 1.819.544Zm5.913 10.883c.107.06.25.106.433.136.181.03.363.045.545.045.364 0 .667-.075.91-.227.243-.15.44-.4.591-.748l.576-1.328-4.51-11.073h2.433l3.275 8.32 3.274-8.32h2.456l-5.344 12.99c0 .016-.007.023-.022.023v.046c-.243.559-.538 1.012-.887 1.36a3.079 3.079 0 0 1-1.183.748c-.44.151-.94.227-1.5.227a6.449 6.449 0 0 1-1.41-.159l.364-2.04Z",fill:"#000",...e}),(0,c.jsx)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M8.242 19.82c1.767 0 3.185-.87 4.254-2.871l2.377-4.436v3.762c0 2.218 1.44 3.545 3.665 3.545 1.745 0 3.032-.762 4.275-2.871l5.475-9.22c1.2-2.023.35-3.545-2.29-3.545-1.418 0-2.334.457-3.163 2l-3.774 7.068V6.968c0-1.87-.894-2.784-2.552-2.784-1.308 0-2.355.565-3.162 2.131L9.79 13.252v-6.22c0-2-.829-2.848-2.836-2.848h-4.1c-1.55 0-2.334.718-2.334 2.044 0 1.327.828 2.088 2.334 2.088h1.68v7.937c0 2.24 1.504 3.567 3.707 3.567ZM36.295 4.184c-4.472 0-7.897 3.327-7.897 7.829 0 4.501 3.447 7.807 7.897 7.807s7.852-3.328 7.874-7.807c0-4.502-3.425-7.829-7.874-7.829Zm0 10.83c-1.68 0-2.836-1.262-2.836-3.001 0-1.74 1.156-3.023 2.836-3.023s2.835 1.283 2.835 3.023c0 1.74-1.134 3-2.835 3Zm8.985-3.001c0-4.502 3.425-7.829 7.875-7.829s7.874 3.349 7.874 7.829c0 4.48-3.424 7.807-7.874 7.807-4.45 0-7.875-3.306-7.875-7.807Zm5.061 0c0 1.74 1.112 3 2.814 3 1.68 0 2.835-1.26 2.835-3S54.834 8.99 53.155 8.99c-1.68 0-2.814 1.283-2.814 3.023Z",fill:"#873EFF"})]});function d(e,t="",o){for(const r in e){const n=e[r],a=t?t+"["+r+"]":r;"string"==typeof n||"number"==typeof n?o.append(a,n):"object"==typeof n&&d(n,a,o)}return o}async function h(e,t,o){const r=d(t,"",new FormData),n=await fetch(e,{method:"POST",body:r,...o});return await n.json()}const p=(e,o)=>{"wcSettings"in window&&wcSettings.wcBlocksConfig&&"product"!==e?wp.data.dispatch("core/notices")?.createNotice("error",o,{context:`wc/${e}`}):fetch(t("ajaxUrl"),{method:"POST",body:new URLSearchParams({action:"woopay_express_checkout_button_show_error_notice",_ajax_nonce:t("woopayButtonNonce"),context:e,message:o})}).then((e=>e.json())).then((e=>{if(e.success){const t=document.querySelector(".woocommerce-notices-wrapper"),o=document.createElement("div");o.innerHTML=e.data.notice,t.insertBefore(o,null),t.scrollIntoView({behavior:"smooth",block:"center"})}}))},f=e=>"object"==typeof wcpayExpressCheckoutParams&&wcpayExpressCheckoutParams.hasOwnProperty(e)?wcpayExpressCheckoutParams[e]:null,m=(e,t,o="wcpay_")=>e.toString().replace("%%endpoint%%",o+t),g=e=>{const t=window.wcpayConfig.pay_for_order,o=window.wcpayConfig.order_id,r=window.wcpayConfig.key,n=window.wcpayConfig.billing_email;if(!t||!o||!r)return e;const a=new URL(e);return a.searchParams.append("pay_for_order",t),a.searchParams.append("order_id",o),a.searchParams.append("key",r),a.searchParams.append("billing_email",n),a.href},b=()=>{(()=>{const e=document.cookie.split(";").find((e=>e.includes("skip_woopay")));if(!e)return!1;const t=e?.split("=");return"skip_woopay"===t[0].trim()&&"1"===t[1].trim()})()&&(document.cookie="skip_woopay=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;")},_=e=>"woopay_shortcode_checkout"===e;window.wp.domReady;const y=(e,o={})=>{var r,n,a,i;if(!1===(null!==(r=t("isShopperTrackingEnabled"))&&void 0!==r?r:f("is_shopper_tracking_enabled")))return;const s=null!==(n=t("platformTrackerNonce"))&&void 0!==n?n:null===(a=f("nonce"))||void 0===a?void 0:a.platform_tracker,c=null!==(i=t("ajaxUrl"))&&void 0!==i?i:f("ajax_url"),l=new FormData;l.append("tracksNonce",s),l.append("action","platform_tracks"),l.append("tracksEventName",e),l.append("tracksEventProp",JSON.stringify(o)),fetch(c,{method:"post",body:l}).then((e=>e.json()))},w=()=>{return e=void 0,o=void 0,n=function*(){var e,o,r;let n=(()=>{const e=document.cookie.split(";");for(let t=0;t<e.length;t++){let o=e[t];for(;" "===o.charAt(0);)o=o.substring(1,o.length);if(0===o.indexOf("tk_ai="))return o.substring(6,o.length)}})();if(n){const e={_ut:"anon",_ui:n};return JSON.stringify(e)}const a=null!==(e=t("platformTrackerNonce"))&&void 0!==e?e:null===(o=f("nonce"))||void 0===o?void 0:o.platform_tracker,i=null!==(r=t("ajaxUrl"))&&void 0!==r?r:f("ajax_url"),s=new FormData;s.append("tracksNonce",a),s.append("action","get_identity");try{const e=yield fetch(i,{method:"post",body:s});if(!e.ok)return;const t=yield e.json();return t.success&&t.data&&t.data._ui&&t.data._ut?JSON.stringify(t.data):void 0}catch(e){return}},new((r=void 0)||(r=Promise))((function(t,a){function i(e){try{c(n.next(e))}catch(e){a(e)}}function s(e){try{c(n.throw(e))}catch(e){a(e)}}function c(e){var o;e.done?t(e.value):(o=e.value,o instanceof r?o:new r((function(e){e(o)}))).then(i,s)}c((n=n.apply(e,o||[])).next())}));var e,o,r,n},v=["color","padding","paddingTop","paddingRight","paddingBottom","paddingLeft"],k=["fontFamily","fontSize","lineHeight","letterSpacing","fontWeight","fontVariation","textDecoration","textShadow","textTransform","-webkit-font-smoothing","-moz-osx-font-smoothing","transition"],S=["backgroundColor","border","borderTop","borderRight","borderBottom","borderLeft","borderRadius","borderWidth","borderColor","borderStyle","borderTopWidth","borderTopColor","borderTopStyle","borderRightWidth","borderRightColor","borderRightStyle","borderBottomWidth","borderBottomColor","borderBottomStyle","borderLeftWidth","borderLeftColor","borderLeftStyle","borderTopLeftRadius","borderTopRightRadius","borderBottomRightRadius","borderBottomLeftRadius","outline","outlineOffset","boxShadow"],x={".Label":[...v,...k],".Text":[...v,...k],".Input":[...v,...k,...S],".Error":[...v,...k,...S],".Tab":[...v,...k,...S],".TabIcon":[...v],".TabLabel":[...v,...k],".Block":[...v.slice(1),...S.slice(1)],".Container":[...S],".Header":[...v,...S,...k],".Footer":[...v,...S,...k]},F={".Label":x[".Label"],".Label--floating":[...x[".Label"],"transform"],".Input":[...x[".Input"],"outlineColor","outlineWidth","outlineStyle"],".Error":x[".Error"],".Tab":["backgroundColor","color","fontFamily"],".Tab--selected":["outlineColor","outlineWidth","outlineStyle","backgroundColor","color",...S],".TabIcon":x[".TabIcon"],".TabIcon--selected":["color"],".TabLabel":x[".TabLabel"],".Block":x[".Block"],".Container":x[".Container"],".Header":x[".Header"],".Footer":x[".Footer"],".Footer--link":x[".Text"],".Text":x[".Text"],".Text--redirect":x[".Text"]};function C(e){return C="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},C(e)}var P=/^\s+/,M=/\s+$/;function T(e,t){if(t=t||{},(e=e||"")instanceof T)return e;if(!(this instanceof T))return new T(e,t);var o=function(e){var t={r:0,g:0,b:0},o=1,r=null,n=null,a=null,i=!1,s=!1;return"string"==typeof e&&(e=function(e){e=e.replace(P,"").replace(M,"").toLowerCase();var t,o=!1;if(Z[e])e=Z[e],o=!0;else if("transparent"==e)return{r:0,g:0,b:0,a:0,format:"name"};return(t=ae.rgb.exec(e))?{r:t[1],g:t[2],b:t[3]}:(t=ae.rgba.exec(e))?{r:t[1],g:t[2],b:t[3],a:t[4]}:(t=ae.hsl.exec(e))?{h:t[1],s:t[2],l:t[3]}:(t=ae.hsla.exec(e))?{h:t[1],s:t[2],l:t[3],a:t[4]}:(t=ae.hsv.exec(e))?{h:t[1],s:t[2],v:t[3]}:(t=ae.hsva.exec(e))?{h:t[1],s:t[2],v:t[3],a:t[4]}:(t=ae.hex8.exec(e))?{r:X(t[1]),g:X(t[2]),b:X(t[3]),a:te(t[4]),format:o?"name":"hex8"}:(t=ae.hex6.exec(e))?{r:X(t[1]),g:X(t[2]),b:X(t[3]),format:o?"name":"hex"}:(t=ae.hex4.exec(e))?{r:X(t[1]+""+t[1]),g:X(t[2]+""+t[2]),b:X(t[3]+""+t[3]),a:te(t[4]+""+t[4]),format:o?"name":"hex8"}:!!(t=ae.hex3.exec(e))&&{r:X(t[1]+""+t[1]),g:X(t[2]+""+t[2]),b:X(t[3]+""+t[3]),format:o?"name":"hex"}}(e)),"object"==C(e)&&(ie(e.r)&&ie(e.g)&&ie(e.b)?(t=function(e,t,o){return{r:255*G(e,255),g:255*G(t,255),b:255*G(o,255)}}(e.r,e.g,e.b),i=!0,s="%"===String(e.r).substr(-1)?"prgb":"rgb"):ie(e.h)&&ie(e.s)&&ie(e.v)?(r=Y(e.s),n=Y(e.v),t=function(e,t,o){e=6*G(e,360),t=G(t,100),o=G(o,100);var r=Math.floor(e),n=e-r,a=o*(1-t),i=o*(1-n*t),s=o*(1-(1-n)*t),c=r%6;return{r:255*[o,i,a,a,s,o][c],g:255*[s,o,o,i,a,a][c],b:255*[a,a,s,o,o,i][c]}}(e.h,r,n),i=!0,s="hsv"):ie(e.h)&&ie(e.s)&&ie(e.l)&&(r=Y(e.s),a=Y(e.l),t=function(e,t,o){var r,n,a;function i(e,t,o){return o<0&&(o+=1),o>1&&(o-=1),o<1/6?e+6*(t-e)*o:o<.5?t:o<2/3?e+(t-e)*(2/3-o)*6:e}if(e=G(e,360),t=G(t,100),o=G(o,100),0===t)r=n=a=o;else{var s=o<.5?o*(1+t):o+t-o*t,c=2*o-s;r=i(c,s,e+1/3),n=i(c,s,e),a=i(c,s,e-1/3)}return{r:255*r,g:255*n,b:255*a}}(e.h,r,a),i=!0,s="hsl"),e.hasOwnProperty("a")&&(o=e.a)),o=J(o),{ok:i,format:e.format||s,r:Math.min(255,Math.max(t.r,0)),g:Math.min(255,Math.max(t.g,0)),b:Math.min(255,Math.max(t.b,0)),a:o}}(e);this._originalInput=e,this._r=o.r,this._g=o.g,this._b=o.b,this._a=o.a,this._roundA=Math.round(100*this._a)/100,this._format=t.format||o.format,this._gradientType=t.gradientType,this._r<1&&(this._r=Math.round(this._r)),this._g<1&&(this._g=Math.round(this._g)),this._b<1&&(this._b=Math.round(this._b)),this._ok=o.ok}function E(e,t,o){e=G(e,255),t=G(t,255),o=G(o,255);var r,n,a=Math.max(e,t,o),i=Math.min(e,t,o),s=(a+i)/2;if(a==i)r=n=0;else{var c=a-i;switch(n=s>.5?c/(2-a-i):c/(a+i),a){case e:r=(t-o)/c+(t<o?6:0);break;case t:r=(o-e)/c+2;break;case o:r=(e-t)/c+4}r/=6}return{h:r,s:n,l:s}}function A(e,t,o){e=G(e,255),t=G(t,255),o=G(o,255);var r,n,a=Math.max(e,t,o),i=Math.min(e,t,o),s=a,c=a-i;if(n=0===a?0:c/a,a==i)r=0;else{switch(a){case e:r=(t-o)/c+(t<o?6:0);break;case t:r=(o-e)/c+2;break;case o:r=(e-t)/c+4}r/=6}return{h:r,s:n,v:s}}function L(e,t,o,r){var n=[Q(Math.round(e).toString(16)),Q(Math.round(t).toString(16)),Q(Math.round(o).toString(16))];return r&&n[0].charAt(0)==n[0].charAt(1)&&n[1].charAt(0)==n[1].charAt(1)&&n[2].charAt(0)==n[2].charAt(1)?n[0].charAt(0)+n[1].charAt(0)+n[2].charAt(0):n.join("")}function I(e,t,o,r){return[Q(ee(r)),Q(Math.round(e).toString(16)),Q(Math.round(t).toString(16)),Q(Math.round(o).toString(16))].join("")}function R(e,t){t=0===t?0:t||10;var o=T(e).toHsl();return o.s-=t/100,o.s=K(o.s),T(o)}function D(e,t){t=0===t?0:t||10;var o=T(e).toHsl();return o.s+=t/100,o.s=K(o.s),T(o)}function q(e){return T(e).desaturate(100)}function W(e,t){t=0===t?0:t||10;var o=T(e).toHsl();return o.l+=t/100,o.l=K(o.l),T(o)}function j(e,t){t=0===t?0:t||10;var o=T(e).toRgb();return o.r=Math.max(0,Math.min(255,o.r-Math.round(-t/100*255))),o.g=Math.max(0,Math.min(255,o.g-Math.round(-t/100*255))),o.b=Math.max(0,Math.min(255,o.b-Math.round(-t/100*255))),T(o)}function H(e,t){t=0===t?0:t||10;var o=T(e).toHsl();return o.l-=t/100,o.l=K(o.l),T(o)}function $(e,t){var o=T(e).toHsl(),r=(o.h+t)%360;return o.h=r<0?360+r:r,T(o)}function N(e){var t=T(e).toHsl();return t.h=(t.h+180)%360,T(t)}function O(e,t){if(isNaN(t)||t<=0)throw new Error("Argument to polyad must be a positive number");for(var o=T(e).toHsl(),r=[T(e)],n=360/t,a=1;a<t;a++)r.push(T({h:(o.h+a*n)%360,s:o.s,l:o.l}));return r}function B(e){var t=T(e).toHsl(),o=t.h;return[T(e),T({h:(o+72)%360,s:t.s,l:t.l}),T({h:(o+216)%360,s:t.s,l:t.l})]}function z(e,t,o){t=t||6,o=o||30;var r=T(e).toHsl(),n=360/o,a=[T(e)];for(r.h=(r.h-(n*t>>1)+720)%360;--t;)r.h=(r.h+n)%360,a.push(T(r));return a}function U(e,t){t=t||6;for(var o=T(e).toHsv(),r=o.h,n=o.s,a=o.v,i=[],s=1/t;t--;)i.push(T({h:r,s:n,v:a})),a=(a+s)%1;return i}T.prototype={isDark:function(){return this.getBrightness()<128},isLight:function(){return!this.isDark()},isValid:function(){return this._ok},getOriginalInput:function(){return this._originalInput},getFormat:function(){return this._format},getAlpha:function(){return this._a},getBrightness:function(){var e=this.toRgb();return(299*e.r+587*e.g+114*e.b)/1e3},getLuminance:function(){var e,t,o,r=this.toRgb();return e=r.r/255,t=r.g/255,o=r.b/255,.2126*(e<=.03928?e/12.92:Math.pow((e+.055)/1.055,2.4))+.7152*(t<=.03928?t/12.92:Math.pow((t+.055)/1.055,2.4))+.0722*(o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4))},setAlpha:function(e){return this._a=J(e),this._roundA=Math.round(100*this._a)/100,this},toHsv:function(){var e=A(this._r,this._g,this._b);return{h:360*e.h,s:e.s,v:e.v,a:this._a}},toHsvString:function(){var e=A(this._r,this._g,this._b),t=Math.round(360*e.h),o=Math.round(100*e.s),r=Math.round(100*e.v);return 1==this._a?"hsv("+t+", "+o+"%, "+r+"%)":"hsva("+t+", "+o+"%, "+r+"%, "+this._roundA+")"},toHsl:function(){var e=E(this._r,this._g,this._b);return{h:360*e.h,s:e.s,l:e.l,a:this._a}},toHslString:function(){var e=E(this._r,this._g,this._b),t=Math.round(360*e.h),o=Math.round(100*e.s),r=Math.round(100*e.l);return 1==this._a?"hsl("+t+", "+o+"%, "+r+"%)":"hsla("+t+", "+o+"%, "+r+"%, "+this._roundA+")"},toHex:function(e){return L(this._r,this._g,this._b,e)},toHexString:function(e){return"#"+this.toHex(e)},toHex8:function(e){return function(e,t,o,r,n){var a=[Q(Math.round(e).toString(16)),Q(Math.round(t).toString(16)),Q(Math.round(o).toString(16)),Q(ee(r))];return n&&a[0].charAt(0)==a[0].charAt(1)&&a[1].charAt(0)==a[1].charAt(1)&&a[2].charAt(0)==a[2].charAt(1)&&a[3].charAt(0)==a[3].charAt(1)?a[0].charAt(0)+a[1].charAt(0)+a[2].charAt(0)+a[3].charAt(0):a.join("")}(this._r,this._g,this._b,this._a,e)},toHex8String:function(e){return"#"+this.toHex8(e)},toRgb:function(){return{r:Math.round(this._r),g:Math.round(this._g),b:Math.round(this._b),a:this._a}},toRgbString:function(){return 1==this._a?"rgb("+Math.round(this._r)+", "+Math.round(this._g)+", "+Math.round(this._b)+")":"rgba("+Math.round(this._r)+", "+Math.round(this._g)+", "+Math.round(this._b)+", "+this._roundA+")"},toPercentageRgb:function(){return{r:Math.round(100*G(this._r,255))+"%",g:Math.round(100*G(this._g,255))+"%",b:Math.round(100*G(this._b,255))+"%",a:this._a}},toPercentageRgbString:function(){return 1==this._a?"rgb("+Math.round(100*G(this._r,255))+"%, "+Math.round(100*G(this._g,255))+"%, "+Math.round(100*G(this._b,255))+"%)":"rgba("+Math.round(100*G(this._r,255))+"%, "+Math.round(100*G(this._g,255))+"%, "+Math.round(100*G(this._b,255))+"%, "+this._roundA+")"},toName:function(){return 0===this._a?"transparent":!(this._a<1)&&(V[L(this._r,this._g,this._b,!0)]||!1)},toFilter:function(e){var t="#"+I(this._r,this._g,this._b,this._a),o=t,r=this._gradientType?"GradientType = 1, ":"";if(e){var n=T(e);o="#"+I(n._r,n._g,n._b,n._a)}return"progid:DXImageTransform.Microsoft.gradient("+r+"startColorstr="+t+",endColorstr="+o+")"},toString:function(e){var t=!!e;e=e||this._format;var o=!1,r=this._a<1&&this._a>=0;return t||!r||"hex"!==e&&"hex6"!==e&&"hex3"!==e&&"hex4"!==e&&"hex8"!==e&&"name"!==e?("rgb"===e&&(o=this.toRgbString()),"prgb"===e&&(o=this.toPercentageRgbString()),"hex"!==e&&"hex6"!==e||(o=this.toHexString()),"hex3"===e&&(o=this.toHexString(!0)),"hex4"===e&&(o=this.toHex8String(!0)),"hex8"===e&&(o=this.toHex8String()),"name"===e&&(o=this.toName()),"hsl"===e&&(o=this.toHslString()),"hsv"===e&&(o=this.toHsvString()),o||this.toHexString()):"name"===e&&0===this._a?this.toName():this.toRgbString()},clone:function(){return T(this.toString())},_applyModification:function(e,t){var o=e.apply(null,[this].concat([].slice.call(t)));return this._r=o._r,this._g=o._g,this._b=o._b,this.setAlpha(o._a),this},lighten:function(){return this._applyModification(W,arguments)},brighten:function(){return this._applyModification(j,arguments)},darken:function(){return this._applyModification(H,arguments)},desaturate:function(){return this._applyModification(R,arguments)},saturate:function(){return this._applyModification(D,arguments)},greyscale:function(){return this._applyModification(q,arguments)},spin:function(){return this._applyModification($,arguments)},_applyCombination:function(e,t){return e.apply(null,[this].concat([].slice.call(t)))},analogous:function(){return this._applyCombination(z,arguments)},complement:function(){return this._applyCombination(N,arguments)},monochromatic:function(){return this._applyCombination(U,arguments)},splitcomplement:function(){return this._applyCombination(B,arguments)},triad:function(){return this._applyCombination(O,[3])},tetrad:function(){return this._applyCombination(O,[4])}},T.fromRatio=function(e,t){if("object"==C(e)){var o={};for(var r in e)e.hasOwnProperty(r)&&(o[r]="a"===r?e[r]:Y(e[r]));e=o}return T(e,t)},T.equals=function(e,t){return!(!e||!t)&&T(e).toRgbString()==T(t).toRgbString()},T.random=function(){return T.fromRatio({r:Math.random(),g:Math.random(),b:Math.random()})},T.mix=function(e,t,o){o=0===o?0:o||50;var r=T(e).toRgb(),n=T(t).toRgb(),a=o/100;return T({r:(n.r-r.r)*a+r.r,g:(n.g-r.g)*a+r.g,b:(n.b-r.b)*a+r.b,a:(n.a-r.a)*a+r.a})},T.readability=function(e,t){var o=T(e),r=T(t);return(Math.max(o.getLuminance(),r.getLuminance())+.05)/(Math.min(o.getLuminance(),r.getLuminance())+.05)},T.isReadable=function(e,t,o){var r,n,a,i,s,c=T.readability(e,t);switch(n=!1,(a=o,"AA"!==(i=((a=a||{level:"AA",size:"small"}).level||"AA").toUpperCase())&&"AAA"!==i&&(i="AA"),"small"!==(s=(a.size||"small").toLowerCase())&&"large"!==s&&(s="small"),r={level:i,size:s}).level+r.size){case"AAsmall":case"AAAlarge":n=c>=4.5;break;case"AAlarge":n=c>=3;break;case"AAAsmall":n=c>=7}return n},T.mostReadable=function(e,t,o){var r,n,a,i,s=null,c=0;n=(o=o||{}).includeFallbackColors,a=o.level,i=o.size;for(var l=0;l<t.length;l++)(r=T.readability(e,t[l]))>c&&(c=r,s=T(t[l]));return T.isReadable(e,s,{level:a,size:i})||!n?s:(o.includeFallbackColors=!1,T.mostReadable(e,["#fff","#000"],o))};var Z=T.names={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"0ff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000",blanchedalmond:"ffebcd",blue:"00f",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",burntsienna:"ea7e5d",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"0ff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkgrey:"a9a9a9",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkslategrey:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dimgrey:"696969",dodgerblue:"1e90ff",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"f0f",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",grey:"808080",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgray:"d3d3d3",lightgreen:"90ee90",lightgrey:"d3d3d3",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslategray:"789",lightslategrey:"789",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"0f0",limegreen:"32cd32",linen:"faf0e6",magenta:"f0f",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370db",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"db7093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",rebeccapurple:"663399",red:"f00",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",slategrey:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",wheat:"f5deb3",white:"fff",whitesmoke:"f5f5f5",yellow:"ff0",yellowgreen:"9acd32"},V=T.hexNames=function(e){var t={};for(var o in e)e.hasOwnProperty(o)&&(t[e[o]]=o);return t}(Z);function J(e){return e=parseFloat(e),(isNaN(e)||e<0||e>1)&&(e=1),e}function G(e,t){(function(e){return"string"==typeof e&&-1!=e.indexOf(".")&&1===parseFloat(e)})(e)&&(e="100%");var o=function(e){return"string"==typeof e&&-1!=e.indexOf("%")}(e);return e=Math.min(t,Math.max(0,parseFloat(e))),o&&(e=parseInt(e*t,10)/100),Math.abs(e-t)<1e-6?1:e%t/parseFloat(t)}function K(e){return Math.min(1,Math.max(0,e))}function X(e){return parseInt(e,16)}function Q(e){return 1==e.length?"0"+e:""+e}function Y(e){return e<=1&&(e=100*e+"%"),e}function ee(e){return Math.round(255*parseFloat(e)).toString(16)}function te(e){return X(e)/255}var oe,re,ne,ae=(re="[\\s|\\(]+("+(oe="(?:[-\\+]?\\d*\\.\\d+%?)|(?:[-\\+]?\\d+%?)")+")[,|\\s]+("+oe+")[,|\\s]+("+oe+")\\s*\\)?",ne="[\\s|\\(]+("+oe+")[,|\\s]+("+oe+")[,|\\s]+("+oe+")[,|\\s]+("+oe+")\\s*\\)?",{CSS_UNIT:new RegExp(oe),rgb:new RegExp("rgb"+re),rgba:new RegExp("rgba"+ne),hsl:new RegExp("hsl"+re),hsla:new RegExp("hsla"+ne),hsv:new RegExp("hsv"+re),hsva:new RegExp("hsva"+ne),hex3:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex6:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,hex4:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex8:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/});function ie(e){return!!ae.CSS_UNIT.exec(e)}const se={default:{hiddenContainer:"#wcpay-hidden-div",hiddenInput:"#wcpay-hidden-input",hiddenInvalidInput:"#wcpay-hidden-invalid-input",hiddenValidActiveLabel:"#wcpay-hidden-valid-active-label"},classicCheckout:{appendTarget:".woocommerce-billing-fields__field-wrapper",upeThemeInputSelector:"#billing_first_name",upeThemeLabelSelector:".woocommerce-checkout .form-row label",upeThemeTextSelectors:["#payment .payment_methods li .payment_box fieldset",".woocommerce-checkout .form-row"],rowElement:"p",validClasses:["form-row"],invalidClasses:["form-row","woocommerce-invalid","woocommerce-invalid-required-field"],backgroundSelectors:["li.wc_payment_method .wc-payment-form","li.wc_payment_method .payment_box","#payment","#order_review","form.checkout","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:["#place_order"],linkSelectors:["a"],pmmeRelativeTextSizeSelector:".wc_payment_method > label"},blocksCheckout:{appendTarget:".wc-block-checkout__contact-fields",upeThemeInputSelector:".wc-block-components-text-input #email",upeThemeLabelSelector:".wc-block-components-text-input label",upeThemeTextSelectors:[".wc-block-components-checkout-step__description",".wc-block-components-text-input",".wc-block-components-radio-control__label",".wc-block-checkout__terms"],rowElement:"div",validClasses:["wc-block-components-text-input","is-active"],invalidClasses:["wc-block-components-text-input","has-error"],alternateSelectors:{appendTarget:"#billing.wc-block-components-address-form",upeThemeInputSelector:"#billing-first_name",upeThemeLabelSelector:".wc-block-components-checkout-step__description"},backgroundSelectors:["#payment-method .wc-block-components-radio-control-accordion-option","#payment-method","form.wc-block-checkout__form",".wc-block-checkout","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:[".wc-block-components-checkout-place-order-button"],linkSelectors:["a"],containerSelectors:[".wp-block-woocommerce-checkout-order-summary-block"],pmmeRelativeTextSizeSelector:".wc-block-components-radio-control__label-group"},bnplProductPage:{appendTarget:".product .cart .quantity",upeThemeInputSelector:".product .cart .quantity .qty",upeThemeLabelSelector:".product .cart .quantity label",upeThemeTextSelectors:[".product .cart .quantity"],rowElement:"div",validClasses:["input-text"],invalidClasses:["input-text","has-error"],backgroundSelectors:["#payment-method-message","#main > .product > div.summary.entry-summary","#main > .product","#main","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:[".single_add_to_cart_button"],linkSelectors:["a"]},bnplClassicCart:{appendTarget:".cart .quantity",upeThemeInputSelector:".cart .quantity .qty",upeThemeLabelSelector:".cart .quantity label",upeThemeTextSelectors:[".cart .quantity"],rowElement:"div",validClasses:["input-text"],invalidClasses:["input-text","has-error"],backgroundSelectors:["#payment-method-message","#main .entry-content .cart_totals","#main .entry-content","#main","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:[".checkout-button"],linkSelectors:["a"],containerSelectors:[".shop_table"]},bnplCartBlock:{appendTarget:".wc-block-cart .wc-block-components-quantity-selector",upeThemeInputSelector:".wc-block-cart .wc-block-components-quantity-selector .wc-block-components-quantity-selector__input",upeThemeLabelSelector:".wc-block-components-text-input",upeThemeTextSelectors:[".wc-block-components-text-input"],rowElement:"div",validClasses:["wc-block-components-text-input"],invalidClasses:["wc-block-components-text-input","has-error"],backgroundSelectors:[".wc-block-components-bnpl-wrapper",".wc-block-components-order-meta",".wc-block-components-totals-wrapper",".wp-block-woocommerce-cart-order-summary-block",".wp-block-woocommerce-cart-totals-block",".wp-block-woocommerce-cart .wc-block-cart",".wp-block-woocommerce-cart","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:[".wc-block-cart__submit-button"],linkSelectors:["a"],containerSelectors:[".wp-block-woocommerce-cart-line-items-block"]},wooPayClassicCheckout:{appendTarget:".woocommerce-billing-fields__field-wrapper",upeThemeInputSelector:"#billing_first_name",upeThemeLabelSelector:".woocommerce-checkout .form-row label",upeThemeTextSelectors:[".woocommerce-checkout .form-row"],rowElement:"p",validClasses:["form-row"],invalidClasses:["form-row","woocommerce-invalid","woocommerce-invalid-required-field"],backgroundSelectors:["#customer_details","#order_review","form.checkout","body"],headingSelectors:["h1","h2","h3","h4","h5","h6"],buttonSelectors:["#place_order"],linkSelectors:["a"],containerSelectors:[".woocommerce-checkout-review-order-table"],headerSelectors:[".site-header","header > div"],footerSelectors:[".site-footer","footer > div"],footerLink:[".site-footer a","footer a"]},updateSelectors:function(e,t){return e.hasOwnProperty("alternateSelectors")&&(Object.entries(e.alternateSelectors).forEach((o=>{const[r,n]=o;t.querySelector(e[r])||(e[r]=n)})),delete e.alternateSelectors),e},getSelectors:function(e,t){let o=this.blocksCheckout;switch(e){case"blocks_checkout":o=this.blocksCheckout;break;case"shortcode_checkout":o=this.classicCheckout;break;case"bnpl_product_page":o=this.bnplProductPage;break;case"bnpl_classic_cart":o=this.bnplClassicCart;break;case"bnpl_cart_block":o=this.bnplCartBlock;break;case"woopay_shortcode_checkout":o=this.wooPayClassicCheckout}return{...this.default,...this.updateSelectors(o,t)}}},ce={getHiddenContainer:function(e,t){const o=t.createElement("div");return o.setAttribute("id",this.getIDFromSelector(e)),o.style.border=0,o.style.clip="rect(0 0 0 0)",o.style.height="1px",o.style.margin="-1px",o.style.overflow="hidden",o.style.padding="0",o.style.position="absolute",o.style.width="1px",o},createRow:function(e,t=[],o){const r=o.createElement(e);return t.length&&r.classList.add(...t),r},appendClone:function(e,t,o,r){const n=r.querySelector(t);if(n){const t=n.cloneNode(!0);t.id=this.getIDFromSelector(o),t.value="",e.appendChild(t)}},getIDFromSelector:function(e){return e.startsWith("#")||e.startsWith(".")?e.slice(1):e},init:function(e,t){const o=se.getSelectors(e),r=t.querySelector(o.appendTarget),n=t.querySelector(o.upeThemeInputSelector);if(!r||!n)return;t.querySelector(o.hiddenContainer)&&this.cleanup(t);const a=this.getHiddenContainer(o.hiddenContainer,t);r.appendChild(a);const i=this.createRow(o.rowElement,o.validClasses,t);a.appendChild(i);const s=this.createRow(o.rowElement,o.invalidClasses,t);a.appendChild(s),this.appendClone(i,o.upeThemeInputSelector,o.hiddenInput,t),this.appendClone(i,o.upeThemeLabelSelector,o.hiddenValidActiveLabel,t),this.appendClone(s,o.upeThemeInputSelector,o.hiddenInvalidInput,t),this.appendClone(s,o.upeThemeLabelSelector,o.hiddenInvalidInput,t),t.querySelector(o.hiddenInput).style.transition="none"},cleanup:function(e){const t=e.querySelector(se.default.hiddenContainer);t&&t.remove()}},le=(e,t,o=null,r)=>{const n=(Array.isArray(e)?e:[e]).map((e=>r.querySelector(e))).filter(Boolean);if(!n.length)return{};const a=r.defaultView||window,i=F[t],s=n[0],c=a.getComputedStyle(s),l={};if(i.forEach((e=>{const t=e.replace(/[A-Z]/g,(e=>`-${e.toLowerCase()}`)),o=c.getPropertyValue(t);if(o)if("color"!==e)if("lineHeight"!==e||"0"!==o&&"0px"!==o)l[e]=o;else for(let t=1;t<n.length;t++){const o=a.getComputedStyle(n[t]).getPropertyValue("line-height");if("0"!==o&&"0px"!==o){l[e]=o;break}}else l[e]=(e=>{const t=e.match(/^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0?(\.\d+)?|1?(\.0+)?)\s*\)$/);if(t){const o=t[4]||1;e=`rgb(${t.slice(1,4).map((e=>Math.round(e*o+255*(1-o)))).join(", ")})`}return e})(o)})),".Input"===t||".Tab--selected"===t){const e=((e,t="solid",o)=>e&&o?[e,t,o].join(" "):"")(l.outlineWidth,l.outlineStyle,l.outlineColor);""!==e&&(l.outline=e),delete l.outlineWidth,delete l.outlineColor,delete l.outlineStyle}const u=c.getPropertyValue("text-indent");return"0px"!==u&&"0px"===l.paddingLeft&&"0px"===l.paddingRight&&(l.paddingLeft=u,l.paddingRight=u),".Block"===t&&(l.backgroundColor=o),l},ue=(e,t=!1,o=document)=>{const r=se.getSelectors(e,o);ce.init(e,o);const n=le(r.hiddenInput,".Input",null,o),a=le(r.hiddenInvalidInput,".Input",null,o),i=le(r.upeThemeLabelSelector,".Label",null,o),s={fontSize:i.fontSize},c=le(r.upeThemeTextSelectors,".Text",null,o),l=le(r.upeThemeInputSelector,".Tab",null,o),u=le(r.hiddenInput,".Tab--selected",null,o),d=(e=>{const t=Object.assign({},e);if(!e.backgroundColor||!e.color)return e;const o=((e,t)=>{const o={backgroundColor:e,color:t},r=T(e),n=T(t);if(!r.isValid()||!n.isValid())return{backgroundColor:"",color:""};const a=r.getBrightness()>50?T(r).darken(7):T(r).lighten(7),i=T.mostReadable(a,[n],{includeFallbackColors:!0});return o.backgroundColor=a.toRgbString(),o.color=i.toRgbString(),o})(e.backgroundColor,e.color);return t.backgroundColor=o.backgroundColor,t.color=o.color,t})(l),h={color:d.color},p={color:u.color},f=((e,t=document)=>{let o=null,r=0;for(;!o&&r<e.length;){const n=t.querySelector(e[r]);if(!n){r++;continue}const a=(t.defaultView||window).getComputedStyle(n).backgroundColor;a&&T(a).getAlpha()>0&&(o=a),r++}return o||"#ffffff"})(r.backgroundSelectors,o),m=le(r.headingSelectors,".Label",null,o),g=le(r.upeThemeLabelSelector,".Block",f,o),b=le(r.buttonSelectors,".Input",null,o),_=le(r.linkSelectors,".Label",null,o),y=le(r.containerSelectors,".Container",null,o),w=le(r.headerSelectors,".Header",null,o),v=le(r.footerSelectors,".Footer",null,o),k=le(r.footerLink,".Footer--link",null,o),S={colorBackground:f,colorText:c.color,fontFamily:c.fontFamily,fontSizeBase:c.fontSize};r.pmmeRelativeTextSizeSelector&&S.fontSizeBase&&(S.fontSizeBase=function(e,t,o=.875,r){const n=parseFloat(t);if(isNaN(n))return t;const a=r.querySelector(e);if(!a)return n*o+"px";const i=window.getComputedStyle(a).getPropertyValue("font-size"),s=parseFloat(i)*o;return isNaN(s)?t:n>s?`${s}px`:`${n}px`}(r.pmmeRelativeTextSizeSelector,c.fontSize,.875,o));const x="blocks_checkout"===e;let F={variables:S,theme:(C=f,T(C).getBrightness()>125?"stripe":"night"),labels:x?"floating":"above",rules:JSON.parse(JSON.stringify({".Input":n,".Input--invalid":a,".Label":i,".Label--resting":s,".Block":g,".Tab":l,".Tab:hover":d,".Tab--selected":u,".TabIcon:hover":h,".TabIcon--selected":p,".Text":c,".Text--redirect":c}))};var C;return x&&(F=((e,t)=>{if(e.rules[".Label--floating"]=t,e.rules[".Label--floating"].transform&&"none"!==e.rules[".Label--floating"].transform){const t=e.rules[".Label--floating"].transform.match(/matrix\((.+)\)/);if(t&&t[1]){const o=t[1].split(", "),r=(parseFloat(o[0])+parseFloat(o[3]))/2,n=parseFloat(e.rules[".Label--floating"].lineHeight),a=Math.floor(n*r);e.rules[".Label--floating"].lineHeight=`${a}px`,e.rules[".Label--floating"].fontSize=`${a}px`}delete e.rules[".Label--floating"].transform}if(e.rules[".Input"].paddingTop&&(e.rules[".Input"].paddingTop=`calc(${e.rules[".Input"].paddingTop} - ${e.rules[".Label--floating"].lineHeight} - 4px - 1px)`),e.rules[".Input"].paddingBottom){var o;const t=parseFloat(e.rules[".Input"].paddingBottom);e.rules[".Input"].paddingBottom=t-1+"px";const r=null!==(o=e.rules[".Label"].marginTop)&&void 0!==o?o:"0";e.rules[".Label"].marginTop=`${Math.floor((t-1)/3)}px`,e.rules[".Label--floating"].marginTop=r}return e})(F,le(r.hiddenValidActiveLabel,".Label--floating",null,o))),t&&(F.rules={...F.rules,".Heading":m,".Header":w,".Footer":v,".Footer-link":k,".Button":b,".Link":_,".Container":y}),ce.cleanup(o),F},de=()=>document.querySelector(".wp-block-woocommerce-checkout")?"blocks_checkout":document.querySelector(".woocommerce-billing-fields")?"woopay_shortcode_checkout":document.querySelector(".wp-block-woocommerce-cart")?"bnpl_cart_block":document.querySelector(".woocommerce-cart-form")?"bnpl_classic_cart":document.querySelector(".single-product")?"bnpl_product_page":void 0,he=async(e,o,r)=>{const a=await w();let i="";const s=document.body,c=document.createElement("div");c.setAttribute("role","dialog"),c.setAttribute("aria-modal","true"),c.classList.add("woopay-otp-iframe-wrapper");const l=document.createElement("iframe");l.title=(0,n.__)("WooPay SMS code verification","woocommerce-payments"),l.classList.add("woopay-otp-iframe"),l.classList.add("intrinsic-ignore");const u=768;let d=!0;const f=()=>{(u<=window.innerWidth&&d||u>window.innerWidth&&!d)&&(d=!d,l.contentWindow.postMessage({action:"setHeader",value:d},t("woopayHost"))),document.body.style.overflow="hidden"},b=()=>{if(!l)return;if(u>window.innerWidth)return l.style.left="0",l.style.right="",void(l.style.top="0");const e=l.getBoundingClientRect();l.style.top=Math.floor(window.innerHeight/2-e.height/2)+"px",l.style.left=Math.floor(window.innerWidth/2-e.width/2)+"px"};l.addEventListener("load",(()=>{d=!0;const e=de(),o=_(e)&&t("isWooPayGlobalThemeSupportEnabled")?ue(e,!0):null;t("isWoopayFirstPartyAuthEnabled")&&h(m(t("wcAjaxUrl"),"get_woopay_session"),{_ajax_nonce:t("woopaySessionNonce"),order_id:t("order_id"),key:t("key"),billing_email:t("billing_email"),appearance:o}).then((e=>{e?.data?.session&&l.contentWindow.postMessage({action:"setSessionData",value:e},t("woopayHost"))})),f(),window.addEventListener("resize",f),b(),window.addEventListener("resize",b),l.classList.add("open")})),c.insertBefore(l,null);const y=()=>{window.removeEventListener("resize",f),window.removeEventListener("resize",b),window.removeEventListener("pageshow",k),window.removeEventListener("message",v),document.removeEventListener("keyup",S),c.remove(),l.classList.remove("open"),document.body.style.overflow=""};function v(r){if(t("woopayHost").startsWith(r.origin))switch(r.data.action){case"otp_email_submitted":i=r.data.userEmail;break;case"redirect_to_woopay_skip_session_init":r.data.redirectUrl&&(window.location=g(r.data.redirectUrl));break;case"redirect_to_platform_checkout":case"redirect_to_woopay":e.initWooPay(i||r.data.userEmail,r.data.platformCheckoutUserSession).then((e=>{if(document.querySelector(".woopay-otp-iframe"))if("success"===e.result)window.location=g(e.url);else{const e=(0,n.__)("WooPay is unavailable at this time. Sorry for the inconvenience.","woocommerce-payments");p(o,e),y()}}));break;case"otp_validation_failed":break;case"close_modal":y();break;case"iframe_height":r.data.height>300&&(u<=window.innerWidth?(l.style.height=r.data.height+"px",l.style.top=Math.floor(window.innerHeight/2-r.data.height/2)+"px"):(l.style.height="",l.style.top=""))}}function k(e){e.persisted&&y()}function S(e){"Escape"===e.key&&y()&&e.stopPropagation()}c.addEventListener("click",y),((e="")=>{if(document.querySelector(".woopay-otp-iframe"))return;window.addEventListener("pageshow",k),window.addEventListener("message",v),document.addEventListener("keyup",S);const r=window.document.documentElement.clientWidth,n=window.document.documentElement.clientHeight,d=new URLSearchParams;var h;d.append("testMode",t("testMode")),d.append("needsHeader",u>window.innerWidth),d.append("wcpayVersion",t("wcpayVersionNumber")),e&&(h=e,new RegExp(/^([a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+(\.[a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)*|"((([ \t]*\r\n)?[ \t]+)?([\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*(([ \t]*\r\n)?[ \t]+)?")@(([a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.)+([a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[0-9a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]){2,}\.?$/i).test(h))&&(i=e,d.append("email",e)),d.append("is_blocks",!!window.wcSettings?.wcBlocksConfig),d.append("is_express","true"),d.append("express_context",o),d.append("source_url",window.location.href),d.append("viewport",`${r}x${n}`),a&&d.append("tracksUserIdentity",a),l.src=`${t("woopayHost")}/otp/?${d.toString()}`,s.insertBefore(c,null),b(),l.focus()})(await(async e=>{if("true"===window.wcpayConfig?.pay_for_order)return window.wcpayCustomerData?.email;const t=await(o=e,o?new Promise((e=>{if(document.querySelector(o))return e(document.querySelector(o));const t=document.querySelector('[data-block-name="woocommerce/checkout"]');if(!t)return e(null);new MutationObserver(((t,r)=>{document.querySelector(o)&&(e(document.querySelector(o)),r.disconnect())})).observe(t,{childList:!0,subtree:!0})})):null);var o;return t?.value})(r)||t("woopaySessionEmail"))};var pe=o(9517),fe=o.n(pe);function me(e){return e.startsWith("{{/")?{type:"componentClose",value:e.replace(/\W/g,"")}:e.endsWith("/}}")?{type:"componentSelfClosing",value:e.replace(/\W/g,"")}:e.startsWith("{{")?{type:"componentOpen",value:e.replace(/\W/g,"")}:{type:"string",value:e}}function ge(e,t){let o,r,n=[];for(let a=0;a<e.length;a++){const i=e[a];if("string"!==i.type){if(void 0===t[i.value])throw new Error(`Invalid interpolation, missing component node: \`${i.value}\``);if("object"!=typeof t[i.value])throw new Error(`Invalid interpolation, component node must be a ReactElement or null: \`${i.value}\``);if("componentClose"===i.type)throw new Error(`Missing opening component token: \`${i.value}\``);if("componentOpen"===i.type){o=t[i.value],r=a;break}n.push(t[i.value])}else n.push(i.value)}if(o){const i=function(e,t){const o=t[e];let r=0;for(let n=e+1;n<t.length;n++){const e=t[n];if(e.value===o.value){if("componentOpen"===e.type){r++;continue}if("componentClose"===e.type){if(0===r)return n;r--}}}throw new Error("Missing closing component token `"+o.value+"`")}(r,e),s=ge(e.slice(r+1,i),t),c=(0,a.cloneElement)(o,{},s);if(n.push(c),i<e.length-1){const o=ge(e.slice(i+1),t);n=n.concat(o)}}return n=n.filter(Boolean),0===n.length?null:1===n.length?n[0]:(0,a.createElement)(a.Fragment,null,...n)}function be(e){const{mixedString:t,components:o,throwErrors:r}=e;if(!o)return t;if("object"!=typeof o){if(r)throw new Error(`Interpolation Error: unable to process \`${t}\` because components is not an object`);return t}const n=function(e){return e.split(/(\{\{\/?\s*\w+\s*\/?\}\})/g).map(me)}(t);try{return ge(n,o)}catch(e){if(r)throw new Error(`Interpolation Error: unable to process \`${t}\` because of error \`${e.message}\``);return t}}function _e(e){window.WooPayConnect||(window.WooPayConnect={}),window.WooPayConnect.iframeInjectedState=e}function ye(){return window?.WooPayConnect?.postMessageTimeout||5e3}const we=()=>{const e=(0,a.useRef)(),[o,r]=(0,a.useState)("");return(0,a.useEffect)((()=>{(async()=>{const e=t("testMode"),o=t("woopayHost"),n=t("woopayMerchantId"),a=new URLSearchParams({testMode:e,source_url:window.location.href,blogId:n}),i=await w();i&&a.append("tracksUserIdentity",i),r(`${o}/connect/?${a.toString()}`)})()}),[]),(0,a.useEffect)((()=>{if(!e.current)return;const o=e.current;o.addEventListener("load",(()=>{_e(2),window.dispatchEvent(new MessageEvent("message",{source:window,origin:t("woopayHost"),data:{action:"get_iframe_post_message_success",value:e=>o.contentWindow.postMessage(e,t("woopayHost"))}}))}))}),[o]),(0,c.jsx)("iframe",{ref:e,id:"woopay-connect-iframe",src:o,style:{height:0,width:0,border:"none",margin:0,padding:0,overflow:"hidden",display:"block",visibility:"hidden",position:"fixed",pointerEvents:"none",userSelect:"none"},title:(0,n.__)("WooPay Connect Direct Checkout","woocommerce-payments")})},ve=class{iframePostMessage=null;listeners={};constructor(){this.listeners={getIframePostMessageCallback:()=>{},getPostMessageTimeoutCallback:()=>{}},this.removeMessageListener=this.attachMessageListener(),this.injectWooPayConnectIframe()}attachMessageListener(){const e=e=>{t("woopayHost").startsWith(e.origin)&&this.callbackFn(e.data)};return window.addEventListener("message",e),()=>{window.removeEventListener("message",e)}}detachMessageListener(){"function"==typeof this.removeMessageListener&&this.removeMessageListener()}injectWooPayConnectIframe(){const o=window?.WooPayConnect?.iframeInjectedState||0;if(2===o){const e=document.querySelector("#woopay-connect-iframe");return void(e&&(this.iframePostMessage=Promise.resolve((o=>{e.contentWindow.postMessage(o,t("woopayHost"))}))))}if(1===o)return void(this.iframePostMessage=new Promise((e=>{this.listeners.getIframePostMessageCallback=e})));_e(1);const r=document.createElement("div");r.style.visibility="hidden",r.style.position="fixed",r.style.height="0",r.style.width="0",r.style.bottom="0",r.style.right="0",r.id="woopay-connect-iframe-container",document.body.appendChild(r);const n=this;this.iframePostMessage=new Promise((e=>{n.listeners.getIframePostMessageCallback=e})),(0,e.H)(r).render((0,c.jsx)(we,{}))}injectTemporaryWooPayConnectIframe(){let e;const o=new Promise((t=>{e=t})),r=document.createElement("iframe");return r.id="temp-woopay-connect-iframe",r.src=t("woopayHost")+"/connect/",r.height=0,r.width=0,r.border="none",r.margin=0,r.padding=0,r.overflow="hidden",r.display="block",r.visibility="hidden",r.position="fixed",r.pointerEvents="none",r.userSelect="none",r.addEventListener("load",(()=>{e((e=>r.contentWindow.postMessage(e,t("woopayHost"))))})),document.body.appendChild(r),{resolvePostMessagePromise:o,removeTemporaryIframe:()=>{document.body.removeChild(r)}}}async sendMessageAndListenWith(e,t){const o=new Promise(((e,o)=>{let r=!1;const n=setTimeout((()=>{r=!0,o(new Error("WooPayConnectIframe did not respond within the allotted time."))}),ye());this.listeners[t]=t=>{r||(n&&clearTimeout(n),e(t))}}));if("function"!=typeof this.iframePostMessage?.then)throw new Error("iframePostMessage is not set");return(await this.iframePostMessage)(e),await o}async getPostMessageTimeout(){try{return await this.sendMessageAndListenWith({action:"getPostMessageTimeout"},"getPostMessageTimeoutCallback")}catch(e){return null}}callbackFn(e){switch(e.action){case"get_iframe_post_message_success":this.listeners.getIframePostMessageCallback(e.value);break;case"get_post_message_timeout_success":this.listeners.getPostMessageTimeoutCallback(e.value)}}},ke=class extends ve{constructor(){super(),this.listeners={...this.listeners,setRedirectSessionDataCallback:()=>{},setTempThirdPartyCookieCallback:()=>{},getIsWooPayReachableCallback:()=>{},getIsThirdPartyCookiesEnabledCallback:()=>{},setPreemptiveSessionDataCallback:()=>{}}}async isWooPayThirdPartyCookiesEnabled(){const{resolvePostMessagePromise:e,removeTemporaryIframe:t}=this.injectTemporaryWooPayConnectIframe(),o=new Promise(((e,t)=>{let o=!1;const r=setTimeout((()=>{o=!0,t(new Error("WooPayConnectIframe did not respond within the allotted time."))}),ye());this.listeners.setTempThirdPartyCookieCallback=t=>{o||(r&&clearTimeout(r),e(t))}}));if("function"!=typeof e?.then)return!1;const r=await e;r({action:"setTempThirdPartyCookie"});try{if(!await o)return t(),!1}catch(e){return t(),!1}const n=new Promise(((e,t)=>{let o=!1;const r=setTimeout((()=>{o=!0,t(new Error("WooPayConnectIframe did not respond within the allotted time."))}),ye());this.listeners.getIsThirdPartyCookiesEnabledCallback=t=>{o||(r&&clearTimeout(r),e(t))}}));r({action:"getIsThirdPartyCookiesEnabled"});try{return await n}catch(e){return!1}finally{t()}}async sendRedirectSessionDataToWooPay(e){try{return await super.sendMessageAndListenWith({action:"setRedirectSessionData",value:e},"setRedirectSessionDataCallback")}catch(e){return null}}async setPreemptiveSessionData(e){try{return await super.sendMessageAndListenWith({action:"setPreemptiveSessionData",value:e},"setPreemptiveSessionDataCallback")}catch(e){return null}}async isWooPayReachable(){try{return await this.sendMessageAndListenWith({action:"isWooPayReachable"},"getIsWooPayReachableCallback")}catch(e){return!1}}callbackFn(e){switch(super.callbackFn(e),e.action){case"set_redirect_session_data_success":this.listeners.setRedirectSessionDataCallback(e.value);break;case"set_redirect_session_data_error":this.listeners.setRedirectSessionDataCallback({is_error:!0});break;case"set_temp_third_party_cookie_success":this.listeners.setTempThirdPartyCookieCallback(e.value);break;case"get_is_third_party_cookies_enabled_success":this.listeners.getIsThirdPartyCookiesEnabledCallback(e.value);break;case"get_is_woopay_reachable_success":this.listeners.getIsWooPayReachableCallback(e.value);break;case"set_preemptive_session_data_success":this.listeners.setPreemptiveSessionDataCallback(e.value);break;case"set_preemptive_session_data_error":this.listeners.setPreemptiveSessionDataCallback({is_error:!0})}}},Se=class{static sessionConnect;static init(){this.getSessionConnect()}static getSessionConnect(){return this.sessionConnect||(this.sessionConnect=new ke),this.sessionConnect}static async sendPreemptiveSessionDataToWooPay(e){return this.getSessionConnect().setPreemptiveSessionData(e)}static async getWooPaySessionFromMerchant(e){return h(m(t("wcAjaxUrl"),"get_woopay_session"),e)}},xe={default:(0,n.__)("WooPay","woocommerce-payments"),buy:(0,n.__)("Buy with WooPay","woocommerce-payments"),donate:(0,n.__)("Donate with WooPay","woocommerce-payments"),book:(0,n.__)("Book with WooPay","woocommerce-payments")},Fe=({listenForCartChanges:e,isPreview:o=!1,buttonSettings:r,api:i,isProductPage:d=!1,emailSelector:h="#email",buttonAttributes:f})=>{var m;const w="narrow",v="wide",k=(0,a.useRef)(null),S=(0,a.useRef)(null),x=(0,a.useRef)(!1);let{height:F,type:C,theme:P,context:M,radius:T}=r;const[E,A]=(0,a.useState)(!1),[L,I]=(0,a.useState)(v),R=new Map;var D;R.set("40","small"),R.set("48","medium"),R.set("55","large"),void 0!==f&&(F=f.height||F,T=null!==(D=f.borderRadius)&&void 0!==D?D:T);const q=R.get(F?.toString())||"medium",W=null!==(m=xe[C||"default"])&&void 0!==m?m:xe.default,j="dark"===P?l:u,{addToCart:H,getProductData:$}=(e=>({addToCart:t=>e.expressCheckoutAddToCart(t),getProductData:()=>{const e=document.querySelector(".single_add_to_cart_button").value,t=document.querySelector(".bundle_form"),o=document.querySelector(".single_variation_wrap");let r={product_id:e,quantity:document.querySelector(".quantity .qty").value};if(o&&!t)r.product_id=o.querySelector('input[name="product_id"]').value,r.attributes=document.querySelector(".variations_form")?(()=>{const e=document.querySelector(".variations_form")?.querySelectorAll(".variations select"),t={};return e&&e.forEach((e=>{const o=e.name,r=e.value||"";t[o]=r})),t})():[];else{const e=new FormData(document.querySelector("form.cart"));e.delete("add-to-cart");const t={};for(const o of e.entries())t[o[0]]=o[1];r={...r,...t}}const a=document.querySelector("form.cart");return!(a&&(new FormData(a).forEach(((e,t)=>{if(/^(addon-|wc_)/.test(t))if(/\[\]$/.test(t)){const o=t.substring(0,t.length-2);r[o]?r[o].push(e):r[o]=[e]}else r[t]=e})),!(e=>{const t=["wc_gc_giftcard_to","wc_gc_giftcard_from","wc_gc_giftcard_to_multiple"];for(const o of t)if(e.hasOwnProperty(o)&&!e[o])return alert((0,n.__)("Please fill out all required fields","woocommerce-payments")),!1;return e.hasOwnProperty("wc_gc_giftcard_to_multiple")&&!e.wc_gc_giftcard_to_multiple.split(",").every((e=>fe()(e.trim())))?(alert((0,n.__)("Please type only valid emails","woocommerce-payments")),!1):!(e.hasOwnProperty("wc_gc_giftcard_to")&&!fe()(e.wc_gc_giftcard_to)&&(alert((0,n.__)("Please type only valid emails","woocommerce-payments")),1))})(r)))&&r}}))(i),N=(0,a.useRef)($),O=(0,a.useRef)(H);(0,a.useEffect)((()=>{if(!S.current)return;const e=S.current.getBoundingClientRect().width;I(e>140?v:w)}),[w,v]),(0,a.useEffect)((()=>{o||y("woopay_button_load",{source:M})}),[o,M]);const B=(0,a.useCallback)((()=>{if(!d)return!0;const e=document.querySelector(".single_add_to_cart_button");return!e||!e.disabled&&!e.classList.contains("disabled")||(e.classList.contains("wc-variation-is-unavailable")?window.alert(window?.wc_add_to_cart_variation_params?.i18n_unavailable_text||(0,n.__)("Sorry, this product is unavailable. Please choose a different combination.","woocommerce-payments")):window.alert((0,n.__)("Please select your product options before proceeding.","woocommerce-payments")),!1)}),[d]),z=(0,a.useCallback)((e=>{if(e?.preventDefault(),!o&&(y("woopay_button_click",{source:M}),b(),B()))if(d){const e=N.current();if(!e)return;O.current(e).then((e=>{e.error?e.submit&&document.querySelector("form.cart").submit():he(i,M,h)}))}else he(i,M,h)}),[i,M,h,o,d,B]),U=(0,a.useCallback)((r=>{if(r.preventDefault(),o||x.current)return;if(y("woopay_button_click",{source:M}),b(),!B())return;x.current=!0,A(!0);const a=de(),i=_(a)&&t("isWooPayGlobalThemeSupportEnabled")?ue(a,!0):null;if(d){const o=N.current();if(!o)return;"function"==typeof e?.stop&&e.stop(),O.current(o).then((()=>{"function"==typeof e?.start&&e.start(),Se.getWooPaySessionFromMerchant({_ajax_nonce:t("woopaySessionNonce"),appearance:i}).then((async e=>{if(!e?.blog_id||!e?.data?.session)throw k.current=z,new Error(e?.data);{const t=await Se.sendPreemptiveSessionDataToWooPay(e);if(t?.is_error)return z(null),k.current=z,x.current=!1,void A(!1);window.location.href=g(t.redirect_url)}})).catch((()=>{const e=(0,n.__)("Something went wrong. Please try again.","woocommerce-payments");p(M,e),x.current=!1,A(!1)}))}))}else Se.getWooPaySessionFromMerchant({_ajax_nonce:t("woopaySessionNonce"),order_id:t("order_id"),key:t("key"),billing_email:t("billing_email"),appearance:i}).then((async e=>{if(!e?.blog_id||!e?.data?.session)throw k.current=z,new Error(e?.data);{const t=await Se.sendPreemptiveSessionDataToWooPay(e);if(t?.is_error)return z(null),k.current=z,x.current=!1,void A(!1);window.location.href=g(t.redirect_url)}}))?.catch((()=>{const e=(0,n.__)("Something went wrong. Please try again.","woocommerce-payments");p(M,e),x.current=!1,A(!1)}))}),[B,M,o,d,e,z]);return(0,a.useEffect)((()=>{t("isWoopayFirstPartyAuthEnabled")?(k.current=U,Se.init()):k.current=z}),[U,z]),(0,a.useEffect)((()=>{const e=e=>{e?.persisted&&(x.current=!1,A(!1))};return window.addEventListener("pageshow",e),()=>{window.removeEventListener("pageshow",e)}}),[]),(0,c.jsx)("div",{id:"wcpay-woopay-button",children:(0,c.jsx)("button",{ref:S,"aria-label":W,onClick:e=>k.current(e),className:s("woopay-express-button",{"is-loading":E}),"data-type":C,"data-size":q,"data-theme":P,"data-width-type":L,style:{height:`${F}px`,borderRadius:`${T}px`},disabled:E,type:"button",children:E?(0,c.jsx)("span",{className:"wc-block-components-spinner"}):(0,c.jsx)("div",{className:"button-content",children:be({mixedString:W.replace(xe.default,"{{wooPayLogo /}}"),components:{wooPayLogo:(0,c.jsx)(j,{})}})})},`${C}-${P}-${q}`)})};class Ce{constructor(e,t){this.options=e,this.stripe=null,this.stripePlatform=null,this.request=t,this.isWooPayRequesting=!1}createStripe(e,t,o="",r=[]){const n={locale:t};return o&&(n.stripeAccount=o),r&&(n.betas=r),new Stripe(e,n)}async getStripeForUPE(e){return this.options.forceNetworkSavedCards=r("paymentMethodsConfig")[e].forceNetworkSavedCards,this.getStripe()}async getStripe(e=!1){let t=0;for(;!window.Stripe;)if(await new Promise((e=>setTimeout(e,100))),t+=100,t>6e5)throw new Error("Stripe object not found");return this.__getStripe(e)}__getStripe(e=!1){const{publishableKey:t,accountId:o,forceNetworkSavedCards:r,locale:n,isStripeLinkEnabled:a}=this.options;if(r&&!e)return this.stripePlatform||(this.stripePlatform=this.createStripe(t,n)),this.stripePlatform;if(!this.stripe){let e=["card_country_event_beta_1"];a&&(e=e.concat(["link_autofill_modal_beta_1"])),this.stripe=this.createStripe(t,n,o,e)}return this.stripe}async loadStripeForExpressCheckout(){try{return this.getStripe(!0)}catch(e){return{error:e}}}confirmIntent(e,o=!1){const r=e.match(/#wcpay-confirm-(pi|si):(.+):(.+):(.+)$/);if(!r)return!0;const n="si"===r[1];let a=r[2];const i=r[3],s=r[4],c=e.indexOf("order-pay"),l=c>-1&&e.substring(c).match(/\d+/);return l&&(a=l[0]),(async()=>{const{locale:e,publishableKey:o}=this.options,r=t("accountIdForIntentConfirmation"),a=await this.getStripe();return n?a.handleNextAction({clientSecret:i}):r?this.createStripe(o,e,r).confirmCardPayment(i):(await this.getStripe(!0)).handleNextAction({clientSecret:i})})().then((e=>{var r;let n=null;e.paymentIntent?.last_payment_error&&(n={message:e.paymentIntent.last_payment_error.message}),"requires_action"===e.paymentIntent?.status&&(n={message:"Payment requires additional action."});const i=e.paymentIntent&&e.paymentIntent.id||e.setupIntent&&e.setupIntent.id||e.error&&e.error.payment_intent&&e.error.payment_intent.id||e.error.setup_intent&&e.error.setup_intent.id,c=null!==(r=f("ajax_url"))&&void 0!==r?r:t("ajaxUrl"),l=t("isChangingPayment");return[this.request(c,{action:"update_order_status",order_id:a,_ajax_nonce:s,intent_id:i,should_save_payment_method:o?"true":"false",is_changing_payment:l?"true":"false"}),n,e.error]})).then((([e,t,o])=>{if(o)throw o;return e.then((e=>{const o="string"==typeof e?JSON.parse(e):e;if(o.error)throw o.error;if(t)throw t;return o.return_url}))}))}async setupIntent(e){const o=await this.request(t("ajaxUrl"),{action:"create_setup_intent","wcpay-payment-method":e,_ajax_nonce:t("createSetupIntentNonce")});if(!o.success)throw o.data.error;if("succeeded"===o.data.status)return o.data;const r=await this.getStripe(),n=await r.confirmCardSetup(o.data.client_secret),{setupIntent:a,error:i}=n;if(i)throw i;return a}saveUPEAppearance(e,o){return this.request(t("ajaxUrl"),{elements_location:o,appearance:JSON.stringify(e),action:"save_upe_appearance",_ajax_nonce:t("saveUPEAppearanceNonce")}).then((e=>e.data)).catch((e=>{throw e.message?e:new Error(e.statusText)}))}initWooPay(e,o){if(!this.isWooPayRequesting){this.isWooPayRequesting=!0;const r=t("wcAjaxUrl"),n=t("initWooPayNonce"),a=de();return this.request(m(r,"init_woopay"),{_wpnonce:n,appearance:t("isWooPayGlobalThemeSupportEnabled")?ue(a,!0):null,email:e,user_session:o,order_id:t("order_id"),key:t("key"),billing_email:t("billing_email")}).finally((()=>{this.isWooPayRequesting=!1}))}}expressCheckoutAddToCart(e){const o=t("wcAjaxUrl"),r=t("addToCartNonce");return this.request(m(o,"add_to_cart"),{security:r,...e})}}const Pe=[];let Me=null;const Te=()=>{((o={})=>{const r=new Ce({publishableKey:t("publishableKey"),accountId:t("accountId"),forceNetworkSavedCards:t("forceNetworkSavedCards"),locale:t("locale")},h),n=document.getElementById("wcpay-woopay-button");if(n){for(;Pe.length>0;)Pe.pop().unmount();const a=(0,e.H)(n);Pe.push(a),a.render((0,c.jsx)(Fe,{listenForCartChanges:o,buttonSettings:t("woopayButton"),api:r,isProductPage:!!n.getAttribute("data-product_page"),emailSelector:"#billing_email"}))}})(Me)};jQuery((e=>{Me={start:()=>{e(document.body).on("updated_cart_totals updated_checkout",Te)},stop:()=>{e(document.body).off("updated_cart_totals updated_checkout",Te)}},Me.start()})),window.addEventListener("load",Te)})()})(); dist/cart.asset.php 0000777 00000000222 15251156605 0010300 0 ustar 00 <?php return array('dependencies' => array('react', 'react-dom', 'wp-dom-ready', 'wp-i18n', 'wp-polyfill'), 'version' => 'bad685e17b0c5ccaaea9'); dist/9d5328fb490cddd43f66.png 0000777 00000524441 15251156605 0011157 0 ustar 00 �PNG IHDR ,( �[A� ��IDATx��w�T��ǿ��{�n/,��{GA+�{��Qc�X���Wc4Ec���c��wAD�����{�9�wva)���>�ݙ;3g�=�9O�=���kt� G�z����Sx��6�Q���py��w������:� ��gN�gq�Z��Bq��-j��#|&Zk�o����6��!n$�^{|���*)���� |���4}���r@�6�t��Ib����.��1�'�X��k�������� ,!�mQ��Y~bZ#��%A[�r�~�oWl�h�x�0���G�����&\�݊�6�L�������dc��v����g��"ۅ�˗����xQ�j�_G��x������<=��;�ѭ�{�K�z�w��g�w��D�<