diff --git a/assets/js/frontend/tokenization-form.js b/assets/js/frontend/tokenization-form.js new file mode 100644 index 00000000000..31eacd3c62e --- /dev/null +++ b/assets/js/frontend/tokenization-form.js @@ -0,0 +1,77 @@ +/*global jQuery, woocommerceTokenizationParams */ +jQuery( function( $ ) { + + var wcTokenizationForm = { + gatewayID: woocommerceTokenizationParams.gatewayID, + userLoggedIn: woocommerceTokenizationParams.userLoggedIn, + + hideForm: function() { + $( '#wc-' + this.gatewayID + '-cc-form, #wc-' + this.gatewayID + '-echeck-form' ).hide(); + }, + + showForm: function() { + $( '#wc-' + this.gatewayID + '-cc-form, #wc-' + this.gatewayID + '-echeck-form' ).show(); + }, + + showSaveNewCheckbox: function() { + $( '#wc-' + this.gatewayID + '-new-payment-method-wrap' ).show(); + }, + + hideSaveNewCheckbox: function() { + $( '#wc-' + this.gatewayID + '-new-payment-method-wrap' ).hide(); + }, + + showSaveNewCheckboxForLoggedInOnly: function() { + if ( this.userLoggedIn ) { + $( '#wc-' + this.gatewayID + '-new-payment-method-wrap' ).show(); + } else { + $( '#wc-' + this.gatewayID + '-new-payment-method-wrap' ).hide(); + } + } + }; + + $( document.body ).on( 'updated_checkout', function() { + + // Make sure a radio button (1st) is selected if there is no is_default for this payment method.. + if ( ! $( 'input[name="wc-' + woocommerceTokenizationParams.gatewayID + '-payment-token"]' ).is( ':checked' ) ) { + $( 'input:radio[name="wc-' + woocommerceTokenizationParams.gatewayID + '-payment-token"]:first' ).attr( 'checked', true ); + if ( 'new' === $( 'input:radio[name="wc-' + woocommerceTokenizationParams.gatewayID + '-payment-token"]:first' ).val() ) { + wcTokenizationForm.showForm(); + wcTokenizationForm.showSaveNewCheckboxForLoggedInOnly(); + } else { + wcTokenizationForm.hideForm(); + wcTokenizationForm.hideSaveNewCheckbox(); + } + } else { + wcTokenizationForm.hideForm(); + wcTokenizationForm.hideSaveNewCheckbox(); + } + + // When a radio button is changed, make sure to show/hide our new CC info area + $( 'input[name="wc-' + woocommerceTokenizationParams.gatewayID + '-payment-token"]' ).change( function () { + if ( 'new' === $( 'input[name="wc-' + woocommerceTokenizationParams.gatewayID + '-payment-token"]:checked' ).val() ) { + wcTokenizationForm.showForm(); + wcTokenizationForm.showSaveNewCheckboxForLoggedInOnly(); + } else { + wcTokenizationForm.hideForm(); + wcTokenizationForm.hideSaveNewCheckbox(); + } + } ); + + // OR if create account is checked + $ ( 'input#createaccount' ).change( function() { + if ( $( this ).is( ':checked' ) ) { + wcTokenizationForm.showSaveNewCheckbox(); + } else { + wcTokenizationForm.hideSaveNewCheckbox(); + } + } ); + + // Don't show the "use new" radio button if we are a guest or only have one method.. + if ( 0 === $( '#wc-' + woocommerceTokenizationParams.gatewayID + '-method-count' ).data( 'count' ) || ! woocommerceTokenizationParams.userLoggedIn ) { + $( '.wc-' + woocommerceTokenizationParams.gatewayID + '-payment-form-new-checkbox-wrap' ).hide(); + } + + } ); + +} ); diff --git a/assets/js/frontend/tokenization-form.min.js b/assets/js/frontend/tokenization-form.min.js new file mode 100644 index 00000000000..7af32c3bf8e --- /dev/null +++ b/assets/js/frontend/tokenization-form.min.js @@ -0,0 +1 @@ +jQuery(function(a){var b={gatewayID:woocommerceTokenizationParams.gatewayID,userLoggedIn:woocommerceTokenizationParams.userLoggedIn,hideForm:function(){a("#wc-"+this.gatewayID+"-cc-form, #wc-"+this.gatewayID+"-echeck-form").hide()},showForm:function(){a("#wc-"+this.gatewayID+"-cc-form, #wc-"+this.gatewayID+"-echeck-form").show()},showSaveNewCheckbox:function(){a("#wc-"+this.gatewayID+"-new-payment-method-wrap").show()},hideSaveNewCheckbox:function(){a("#wc-"+this.gatewayID+"-new-payment-method-wrap").hide()},showSaveNewCheckboxForLoggedInOnly:function(){this.userLoggedIn?a("#wc-"+this.gatewayID+"-new-payment-method-wrap").show():a("#wc-"+this.gatewayID+"-new-payment-method-wrap").hide()}};a(document.body).on("updated_checkout",function(){a('input[name="wc-'+woocommerceTokenizationParams.gatewayID+'-payment-token"]').is(":checked")?(b.hideForm(),b.hideSaveNewCheckbox()):(a('input:radio[name="wc-'+woocommerceTokenizationParams.gatewayID+'-payment-token"]:first').attr("checked",!0),"new"===a('input:radio[name="wc-'+woocommerceTokenizationParams.gatewayID+'-payment-token"]:first').val()?(b.showForm(),b.showSaveNewCheckboxForLoggedInOnly()):(b.hideForm(),b.hideSaveNewCheckbox())),a('input[name="wc-'+woocommerceTokenizationParams.gatewayID+'-payment-token"]').change(function(){"new"===a('input[name="wc-'+woocommerceTokenizationParams.gatewayID+'-payment-token"]:checked').val()?(b.showForm(),b.showSaveNewCheckboxForLoggedInOnly()):(b.hideForm(),b.hideSaveNewCheckbox())}),a("input#createaccount").change(function(){a(this).is(":checked")?b.showSaveNewCheckbox():b.hideSaveNewCheckbox()}),0!==a("#wc-"+woocommerceTokenizationParams.gatewayID+"-method-count").data("count")&&woocommerceTokenizationParams.userLoggedIn||a(".wc-"+woocommerceTokenizationParams.gatewayID+"-payment-form-new-checkbox-wrap").hide()})}); \ No newline at end of file diff --git a/includes/abstracts/abstract-wc-order.php b/includes/abstracts/abstract-wc-order.php index 4db15cb9dde..85b9225492d 100644 --- a/includes/abstracts/abstract-wc-order.php +++ b/includes/abstracts/abstract-wc-order.php @@ -144,6 +144,39 @@ abstract class WC_Abstract_Order { } } + /** + * Returns a list of all payment tokens associated with the current order + * + * @since 2.6 + * @return array An array of payment token objects + */ + public function get_payment_tokens() { + return WC_Payment_Tokens::get_order_tokens( $this->id ); + } + + /** + * Add a payment token to an order + * + * @since 2.6 + * @param WC_Payment_Token $token Payment token object + * @return boolean True if the token was added, false if not + */ + public function add_payment_token( $token ) { + if ( empty( $token ) || ! ( $token instanceof WC_Payment_Token ) ) { + return false; + } + + $token_ids = get_post_meta( $this->id, '_payment_tokens', true ); + if ( empty ( $token_ids ) ) { + $token_ids = array(); + } + $token_ids[] = $token->get_id(); + + update_post_meta( $this->id, '_payment_tokens', $token_ids ); + do_action( 'woocommerce_payment_token_added_to_order', $this->id, $token->get_id(), $token, $token_ids ); + return true; + } + /** * Set the payment method for the order. * diff --git a/includes/abstracts/abstract-wc-payment-gateway.php b/includes/abstracts/abstract-wc-payment-gateway.php index 325e65019a2..ca572655a38 100644 --- a/includes/abstracts/abstract-wc-payment-gateway.php +++ b/includes/abstracts/abstract-wc-payment-gateway.php @@ -102,6 +102,36 @@ abstract class WC_Payment_Gateway extends WC_Settings_API { */ public $view_transaction_url = ''; + /** + * Optional label to show for "new payment method" in the payment + * method/token selection radio selection. + * @var string + */ + public $new_method_label = ''; + + /** + * Contains a users saved tokens for this gateway. + * @var array + */ + protected $tokens = array(); + + /** + * Returns a users saved tokens for this gateway. + * @since 2.6.0 + * @return array + */ + public function get_tokens() { + if ( sizeof( $this->tokens ) > 0 ) { + return $this->tokens; + } + + if ( is_user_logged_in() && $this->supports( 'tokenization' ) && is_checkout() ) { + $this->tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id(), $this->id ); + } + + return $this->tokens; + } + /** * Return the title for admin screens. * @return string @@ -306,13 +336,12 @@ abstract class WC_Payment_Gateway extends WC_Settings_API { * Override this in your gateway if you have some. */ public function payment_fields() { - if ( $description = $this->get_description() ) { echo wpautop( wptexturize( $description ) ); } if ( $this->supports( 'default_credit_card_form' ) ) { - $this->credit_card_form(); + $this->credit_card_form(); // Deprecated, will be removed in a future version. } } @@ -331,48 +360,136 @@ abstract class WC_Payment_Gateway extends WC_Settings_API { } /** - * Core credit card form which gateways can used if needed. - * + * Enqueues our tokenization script to handle some of the new form options. + * @since 2.6.0 + */ + public function tokenization_script() { + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + wp_enqueue_script( + 'woocommerce-tokenization-form', + plugins_url( '/assets/js/frontend/tokenization-form' . $suffix . '.js', WC_PLUGIN_FILE ), + array( 'jquery' ), + WC()->version + ); + wp_localize_script( 'woocommerce-tokenization-form', 'woocommerceTokenizationParams', array( + 'gatewayID' => $this->id, + 'userLoggedIn' => (bool) is_user_logged_in(), + ) ); + } + + /** + * Grab and display our saved payment methods. + * @since 2.6.0 + */ + public function saved_payment_methods() { + $html = '

'; + foreach ( $this->get_tokens() as $token ) { + $html .= $this->saved_payment_method( $token ); + } + $html .= '

'; + $html .= '
'; + echo apply_filters( 'wc_payment_gateway_form_saved_payment_methods_html', $html, $this ); + } + + /** + * Outputs a saved payment method from a token. + * @since 2.6.0 + * @param WC_Payment_Token $token Payment Token + * @return string Generated payment method HTML + */ + public function saved_payment_method( $token ) { + $html = sprintf( + '', + esc_attr( $this->id ), + esc_attr( $token->get_id() ), + checked( $token->is_default(), true, false ) + ); + + $html .= sprintf( '
'; + + return apply_filters( 'wc_payment_gateway_form_saved_payment_method_html', $html, $token, $this ); + } + + /** + * Outputs a saved payment method's title based on the passed token. + * @since 2.6.0 + * @param WC_Payment_Token $token Payment Token + * @return string Generated payment method title HTML + */ + public function saved_payment_method_title( $token ) { + if ( 'CC' == $token->get_type() && is_callable( array( $token, 'get_card_type' ) ) ) { + $type = esc_html__( wc_get_credit_card_type_label( $token->get_card_type() ), 'woocommerce' ); + } else if ( 'eCheck' === $token->get_type() ) { + $type = esc_html__( 'eCheck', 'woocommerce' ); + } + + $type = apply_filters( 'wc_payment_gateway_form_saved_payment_method_title_type_html', $type, $token, $this ); + $title = $type; + + if ( is_callable( array( $token, 'get_last4' ) ) ) { + $title .= ' ' . sprintf( esc_html__( 'ending in %s', 'woocommerce' ), $token->get_last4() ); + } + + if ( is_callable( array( $token, 'get_expiry_month' ) ) && is_callable( array( $token, 'get_expiry_year' ) ) ) { + $title .= ' ' . sprintf( esc_html__( '(expires %s)', 'woocommerce' ), $token->get_expiry_month() . '/' . substr( $token->get_expiry_year(), 2 ) ); + } + + return apply_filters( 'wc_payment_gateway_form_saved_payment_method_title_html', $title, $token, $this ); + } + + /** + * Outputs a checkbox for saving a new payment method to the database. + * @since 2.6.0 + */ + public function save_payment_method_checkbox() { + $html = sprintf( + '

', + esc_attr( $this->id ) + ); + $html .= sprintf( + '', + esc_attr( $this->id ) + ); + $html .= sprintf( + '', + esc_attr( $this->id ), + esc_html__( 'Save to Account', 'woocommerce' ) + ); + $html .= '

'; + echo $html; + } + + /** + * Displays a radio button for entering a new payment method (new CC details) instead of using a saved method. + * Only displayed when a gateway supports tokenization. + * @since 2.6.0 + */ + public function use_new_payment_method_checkbox() { + $label = ( ! empty( $this->new_method_label ) ? esc_html( $this->new_method_label ) : esc_html__( 'Use a new payment method', 'woocommerce' ) ); + $html = ''; + $html .= ''; + echo '
' . $html . '
'; + } + + /** + * Core credit card form which gateways can used if needed. Deprecated - inheirt WC_Payment_Gateway_CC instead. * @param array $args * @param array $fields */ public function credit_card_form( $args = array(), $fields = array() ) { - - wp_enqueue_script( 'wc-credit-card-form' ); - - $default_args = array( - 'fields_have_names' => true, // Some gateways like stripe don't need names as the form is tokenized. - ); - - $args = wp_parse_args( $args, apply_filters( 'woocommerce_credit_card_form_args', $default_args, $this->id ) ); - - $default_fields = array( - 'card-number-field' => '

- - -

', - 'card-expiry-field' => '

- - -

', - 'card-cvc-field' => '

- - -

' - ); - - $fields = wp_parse_args( $fields, apply_filters( 'woocommerce_credit_card_form_fields', $default_fields, $this->id ) ); - ?> -
- id ); ?> - - id ); ?> -
-
- form' ); + $cc_form = new WC_Payment_Gateway_CC; + $cc_form->id = $this->id; + $cc_form->supports = $this->supports; + $cc_form->form(); } + } diff --git a/includes/abstracts/abstract-wc-payment-token.php b/includes/abstracts/abstract-wc-payment-token.php new file mode 100644 index 00000000000..226555b88a7 --- /dev/null +++ b/includes/abstracts/abstract-wc-payment-token.php @@ -0,0 +1,264 @@ +id = $id; + $this->data = $data; + $this->data['type'] = $this->type; + $this->meta = $meta; + } + + /** + * Returns the payment token ID. + * @since 2.6.0 + * @return ID Token ID + */ + public function get_id() { + return absint( $this->id ); + } + + /** + * Returns the raw payment token. + * @since 2.6.0 + * @return string Raw token + */ + public function get_token() { + return $this->data['token']; + } + + /** + * Set the raw payment token. + * @since 2.6.0 + * @param string $token + */ + public function set_token( $token ) { + $this->data['token'] = $token; + } + + /** + * Returns the type of this payment token (CC, eCheck, or something else). + * @since 2.6.0 + * @return string Payment Token Type (CC, eCheck) + */ + public function get_type() { + return isset( $this->data['type'] ) ? $this->data['type'] : ''; + } + + /** + * Returns the user ID associated with the token or false if this token is not associated. + * @since 2.6.0 + * @return int User ID if this token is associated with a user or 0 if no user is associated + */ + public function get_user_id() { + return ( isset( $this->data['user_id'] ) && $this->data['user_id'] > 0 ) ? absint( $this->data['user_id'] ) : 0; + } + + /** + * Set the user ID for the user associated with this order. + * @since 2.6.0 + * @param int $user_id + */ + public function set_user_id( $user_id ) { + $this->data['user_id'] = $user_id; + } + + /** + * Returns the ID of the gateway associated with this payment token. + * @since 2.6.0 + * @return string Gateway ID + */ + public function get_gateway_id() { + return $this->data['gateway_id']; + } + + /** + * Set the gateway ID. + * @since 2.6.0 + * @param string $gateway_id + */ + public function set_gateway_id( $gateway_id ) { + $this->data['gateway_id'] = $gateway_id; + } + + /** + * Returns if the token is marked as default. + * @since 2.6.0 + * @return boolean True if the token is default + */ + public function is_default() { + return ! empty( $this->data['is_default'] ); + } + + /** + * Marks the payment as default or non-default. + * @since 2.6.0 + * @param boolean $is_default True or false + */ + public function set_default( $is_default ) { + $this->data['is_default'] = (bool) $is_default; + } + + /** + * Returns a dump of the token data (combined data and meta). + * @since 2.6.0 + * @return mixed array representation + */ + public function get_data() { + return array_merge( $this->data, array( 'meta' => $this->meta ) ); + } + + /** + * Validate basic token info (token and type are required). + * @since 2.6.0 + * @return boolean True if the passed data is valid + */ + public function validate() { + if ( empty( $this->data['token'] ) ) { + return false; + } + + if ( empty( $this->data['type'] ) ) { + return false; + } + + return true; + } + + /** + * Get a token from the database. + * @since 2.6.0 + * @param int $token_id Token ID + */ + public function read( $token_id ) { + global $wpdb; + if ( $token = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d LIMIT 1;", $token_id ) ) ) { + $this->id = $token->token_id; + $token = (array) $token; + unset( $token['token_id'] ); + $this->data = $token; + $meta = get_metadata( 'payment_token', $token_id ); + $passed_meta = array(); + if ( ! empty( $meta ) ) { + foreach( $meta as $meta_key => $meta_value ) { + $passed_meta[ $meta_key ] = $meta_value[0]; + } + } + $this->meta = $passed_meta; + } + } + + /** + * Update a payment token. + * @since 2.6.0 + * @return True on success, false if validation failed and a payment token could not be updated + */ + public function update() { + if ( false === $this->validate() ) { + return false; + } + + global $wpdb; + + $wpdb->update( $wpdb->prefix . 'woocommerce_payment_tokens', $this->data, array( 'token_id' => $this->get_id() ) ); + foreach ( $this->meta as $meta_key => $meta_value ) { + update_metadata( 'payment_token', $this->get_id(), $meta_key, $meta_value ); + } + + do_action( 'woocommerce_payment_token_updated', $this->get_id() ); + return true; + } + + /** + * Create a new payment token in the database. + * @since 2.6.0 + * @return True on success, false if validation failed and a payment token could not be created + */ + public function create() { + if ( false === $this->validate() ) { + return false; + } + + global $wpdb; + + // Are there any other tokens? If not, set this token as default + if ( ! $this->is_default() && is_user_logged_in() ) { + $default_token = WC_Payment_Tokens::get_customer_default_token( get_current_user_id() ); + if ( is_null( $default_token ) ) { + $this->set_default( true ); + } + } + + $wpdb->insert( $wpdb->prefix . 'woocommerce_payment_tokens', $this->data ); + $this->id = $token_id = $wpdb->insert_id; + foreach ( $this->meta as $meta_key => $meta_value ) { + add_metadata( 'payment_token', $token_id, $meta_key, $meta_value, true ); + } + + do_action( 'woocommerce_payment_token_created', $token_id ); + return true; + } + + /** + * Saves a payment token to the database - does not require you to know if this is a new token or an update token. + * @since 2.6.0 + * @return True on success, false if validation failed and a payment token could not be saved + */ + public function save() { + if ( $this->get_id() > 0 ) { + return $this->update(); + } else { + return $this->create(); + } + } + + /** + * Remove a payment token from the database. + * @since 2.6.0 + */ + public function delete() { + global $wpdb; + $this->read( $this->get_id() ); // Make sure we have a token to return after deletion + $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'token_id' => $this->get_id() ), array( '%d' ) ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokenmeta', array( 'payment_token_id' => $this->get_id() ), array( '%d' ) ); + do_action( 'woocommerce_payment_token_deleted', $this->get_id(), $this ); + } + +} diff --git a/includes/admin/settings/class-wc-settings-checkout.php b/includes/admin/settings/class-wc-settings-checkout.php index ed8f5306532..02b902bb68d 100644 --- a/includes/admin/settings/class-wc-settings-checkout.php +++ b/includes/admin/settings/class-wc-settings-checkout.php @@ -201,6 +201,25 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { 'desc_tip' => true, ), + array( + 'title' => __( 'Delete Payment Method', 'woocommerce' ), + 'desc' => __( 'Endpoint for the delete payment method page', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_delete_payment_method_endpoint', + 'type' => 'text', + 'default' => 'delete-payment-method', + 'desc_tip' => true, + ), + + array( + 'title' => __( 'Set Default Payment Method', 'woocommerce' ), + 'desc' => __( 'Endpoint for the setting a default payment page', 'woocommerce' ), + 'id' => 'woocommerce_myaccount_set_default_payment_method_endpoint', + 'type' => 'text', + 'default' => 'set-default-payment-method', + 'desc_tip' => true, + ), + + array( 'type' => 'sectionend', 'id' => 'checkout_endpoint_options', diff --git a/includes/admin/views/html-admin-page-status-report.php b/includes/admin/views/html-admin-page-status-report.php index 72de1214608..f278552bcca 100644 --- a/includes/admin/views/html-admin-page-status-report.php +++ b/includes/admin/views/html-admin-page-status-report.php @@ -334,6 +334,8 @@ if ( ! defined( 'ABSPATH' ) ) { 'woocommerce_shipping_zones', 'woocommerce_shipping_zone_locations', 'woocommerce_shipping_zone_methods', + 'woocommerce_payment_tokens', + 'woocommerce_payment_tokenmeta', ); foreach ( $tables as $table ) { diff --git a/includes/class-wc-autoloader.php b/includes/class-wc-autoloader.php index f6af30d3d6a..a891fdf3e43 100644 --- a/includes/class-wc-autoloader.php +++ b/includes/class-wc-autoloader.php @@ -83,6 +83,8 @@ class WC_Autoloader { $path = $this->include_path . 'admin/'; } elseif ( strpos( $class, 'wc_cli_' ) === 0 ) { $path = $this->include_path . 'cli/'; + } elseif ( strpos( $class, 'wc_payment_token_' ) === 0 ) { + $path = $this->include_path . 'payment-tokens/'; } if ( empty( $path ) || ( ! $this->load_file( $path . $file ) && strpos( $class, 'wc_' ) === 0 ) ) { diff --git a/includes/class-wc-form-handler.php b/includes/class-wc-form-handler.php index ba42eff65e6..a59ed5c1975 100644 --- a/includes/class-wc-form-handler.php +++ b/includes/class-wc-form-handler.php @@ -359,17 +359,15 @@ class WC_Form_Handler { $payment_method = wc_clean( $_POST['payment_method'] ); $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - // Validate $available_gateways[ $payment_method ]->validate_fields(); // Process if ( wc_notice_count( 'wc_errors' ) == 0 ) { $result = $available_gateways[ $payment_method ]->add_payment_method(); - // Redirect to success/confirmation/payment page if ( $result['result'] == 'success' ) { - wc_add_message( __( 'Payment method added.', 'woocommerce' ) ); + wc_add_notice( __( 'Payment method added.', 'woocommerce' ) ); wp_redirect( $result['redirect'] ); exit(); } diff --git a/includes/class-wc-install.php b/includes/class-wc-install.php index 25f6addaedb..c2f9e5d7c7e 100644 --- a/includes/class-wc-install.php +++ b/includes/class-wc-install.php @@ -484,6 +484,25 @@ CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods ( method_id varchar(255) NOT NULL, method_order bigint(20) NOT NULL, PRIMARY KEY (instance_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokens ( + token_id bigint(20) NOT NULL auto_increment, + gateway_id varchar(255) NOT NULL, + token text NOT NULL, + user_id bigint(20) NOT NULL DEFAULT '0', + type varchar(255) NOT NULL, + is_default tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (token_id), + KEY user_id (user_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokenmeta ( + meta_id bigint(20) NOT NULL auto_increment, + payment_token_id bigint(20) NOT NULL, + meta_key varchar(255) NULL, + meta_value longtext NULL, + PRIMARY KEY (meta_id), + KEY payment_token_id (payment_token_id), + KEY meta_key (meta_key) ) $collate; "; diff --git a/includes/class-wc-payment-gateways.php b/includes/class-wc-payment-gateways.php index a7c613d9708..eb20438d925 100644 --- a/includes/class-wc-payment-gateways.php +++ b/includes/class-wc-payment-gateways.php @@ -146,7 +146,9 @@ class WC_Payment_Gateways { if ( $gateway->is_available() ) { if ( ! is_add_payment_method_page() ) { $_available_gateways[ $gateway->id ] = $gateway; - } elseif( $gateway->supports( 'add_payment_method' ) ) { + } else if( $gateway->supports( 'add_payment_method' ) ) { + $_available_gateways[ $gateway->id ] = $gateway; + } else if ( $gateway->supports( 'tokenization' ) ) { $_available_gateways[ $gateway->id ] = $gateway; } } @@ -166,7 +168,14 @@ class WC_Payment_Gateways { return; } - $current = WC()->session->get( 'chosen_payment_method' ); + if ( is_user_logged_in() ) { + $default_token = WC_Payment_Tokens::get_customer_default_token( get_current_user_id() ); + if ( ! is_null( $default_token ) ) { + $default_token_gateway = $default_token->get_gateway_id(); + } + } + + $current = ( isset( $default_token_gateway ) ? $default_token_gateway : WC()->session->get( 'chosen_payment_method' ) ); if ( $current && isset( $gateways[ $current ] ) ) { $current_gateway = $gateways[ $current ]; diff --git a/includes/class-wc-payment-tokens.php b/includes/class-wc-payment-tokens.php new file mode 100644 index 00000000000..0f54f6c6f47 --- /dev/null +++ b/includes/class-wc-payment-tokens.php @@ -0,0 +1,198 @@ +get_results( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d", + $customer_id + ) ); + + if ( empty( $token_results ) ) { + return array(); + } + + $tokens = array(); + foreach ( $token_results as $token_result ) { + if ( empty( $gateway_id ) || $gateway_id === $token_result->gateway_id ) { + $_token = self::get( $token_result->token_id, $token_result ); + if ( ! empty( $_token ) ) { + $tokens[ $token_result->token_id ] = $_token; + } + } + } + + return apply_filters( 'woocommerce_get_customer_payment_tokens', $tokens, $customer_id ); + } + + /** + * Returns a customers default token or NULL if there is no default token. + * @since 2.6.0 + * @param int $customer_id + * @return WC_Payment_Token|null + */ + public static function get_customer_default_token( $customer_id ) { + if ( $customer_id < 1 ) { + return null; + } + + global $wpdb; + + $token = $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d AND is_default = 1", + $customer_id + ) ); + + if ( $token ) { + return self::get( $token->token_id, $token ); + } else { + return null; + } + } + + /** + * Returns an array of payment token objects associated with the passed order ID. + * @since 2.6.0 + * @param int $order_id Order ID + * @return array Array of token objects + */ + public static function get_order_tokens( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return array(); + } + + $token_ids = get_post_meta( $order_id, '_payment_tokens', true ); + if ( empty ( $token_ids ) ) { + return array(); + } + + global $wpdb; + + $token_ids_as_string = implode( ',', array_map( 'intval', $token_ids ) ); + $token_results = $wpdb->get_results( + "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id IN ( {$token_ids_as_string} )" + ); + + if ( empty( $token_results ) ) { + return array(); + } + + $tokens = array(); + foreach ( $token_results as $token_result ) { + $_token = self::get( $token_result->token_id, $token_result ); + if ( ! empty( $_token ) ) { + $tokens[ $token_result->token_id ] = $_token; + } + } + + return apply_filters( 'woocommerce_get_order_payment_tokens', $tokens, $order_id ); + } + + /** + * Get a token object by ID. + * @since 2.6.0 + * @param int $token_id Token ID + * @return WC_Payment_Token|null Returns a valid payment token or null if no token can be found + */ + public static function get( $token_id, $token_result = null ) { + global $wpdb; + if ( is_null( $token_result ) ) { + $token_result = $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", + $token_id + ) ); + // Still empty? Token doesn't exist? Don't continue + if ( empty( $token_result ) ) { + return null; + } + } + $token_class = 'WC_Payment_Token_' . $token_result->type; + if ( class_exists( $token_class ) ) { + $meta = get_metadata( 'payment_token', $token_id ); + $passed_meta = array(); + if ( ! empty( $meta ) ) { + foreach( $meta as $meta_key => $meta_value ) { + $passed_meta[ $meta_key ] = $meta_value[0]; + } + } + return new $token_class( $token_id, (array) $token_result, $passed_meta ); + } + } + + /** + * Remove a payment token from the database by ID. + * @since 2.6.0 + * @param WC_Payment_Token $token_id Token ID + */ + public static function delete( $token_id ) { + $type = self::get_token_type_by_id( $token_id ); + if ( ! empty ( $type ) ) { + $class = 'WC_Payment_Token_' . $type; + $token = new $class( $token_id ); + $token->delete(); + } + } + + /** + * Loops through all of a users payment tokens and sets is_default to false for all but a specific token. + * @since 2.6.0 + * @param int $user_id User to set a default for + * @param int $token_id The ID of the token that should be default + */ + public static function set_users_default( $user_id, $token_id ) { + $users_tokens = self::get_customer_tokens( $user_id ); + foreach ( $users_tokens as $token ) { + if ( $token_id === $token->get_id() ) { + $token->set_default( true ); + } else { + $token->set_default( false ); + } + $token->update(); + } + } + + /** + * Returns what type (credit card, echeck, etc) of token a token is by ID. + * @since 2.6.0 + * @param int $token_id Token ID + * @return string Type + */ + public static function get_token_type_by_id( $token_id ) { + global $wpdb; + $type = $wpdb->get_var( $wpdb->prepare( + "SELECT type FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", + $token_id + ) ); + return $type; + } + +} diff --git a/includes/class-wc-query.php b/includes/class-wc-query.php index 1645af43adf..ebb36787b16 100644 --- a/includes/class-wc-query.php +++ b/includes/class-wc-query.php @@ -65,17 +65,18 @@ class WC_Query { // Checkout actions. 'order-pay' => get_option( 'woocommerce_checkout_pay_endpoint', 'order-pay' ), 'order-received' => get_option( 'woocommerce_checkout_order_received_endpoint', 'order-received' ), - // My account actions. - 'orders' => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ), - 'view-order' => get_option( 'woocommerce_myaccount_view_order_endpoint', 'view-order' ), - 'downloads' => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ), - 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), - 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), - 'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ), - 'lost-password' => get_option( 'woocommerce_myaccount_lost_password_endpoint', 'lost-password' ), - 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), - 'add-payment-method' => get_option( 'woocommerce_myaccount_add_payment_method_endpoint', 'add-payment-method' ), + 'orders' => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ), + 'view-order' => get_option( 'woocommerce_myaccount_view_order_endpoint', 'view-order' ), + 'downloads' => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ), + 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), + 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), + 'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ), + 'lost-password' => get_option( 'woocommerce_myaccount_lost_password_endpoint', 'lost-password' ), + 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), + 'add-payment-method' => get_option( 'woocommerce_myaccount_add_payment_method_endpoint', 'add-payment-method' ), + 'delete-payment-method' => get_option( 'woocommerce_myaccount_delete_payment_method_endpoint', 'delete-payment-method' ), + 'set-default-payment-method' => get_option( 'woocommerce_myaccount_set_default_payment_method_endpoint', 'set-default-payment-method' ), ); } diff --git a/includes/gateways/class-wc-payment-gateway-cc.php b/includes/gateways/class-wc-payment-gateway-cc.php new file mode 100644 index 00000000000..e560efed26e --- /dev/null +++ b/includes/gateways/class-wc-payment-gateway-cc.php @@ -0,0 +1,85 @@ +supports( 'tokenization' ) && is_checkout(); + + if ( $display_tokenization ) { + $this->tokenization_script(); + if ( is_user_logged_in() ) { + $this->saved_payment_methods(); + } + $this->use_new_payment_method_checkbox(); + } + + $this->form(); + + if ( $display_tokenization ) { + $this->save_payment_method_checkbox(); + } + } + + /** + * Outputs fields for entering credit card information. + * @since 2.6.0 + */ + public function form() { + $html = ''; + $fields = array(); + + $cvc_field = '

+ + +

'; + + $default_fields = array( + 'card-number-field' => '

+ + +

', + 'card-expiry-field' => '

+ + +

' + ); + + if ( ! $this->supports( 'credit_card_form_cvc_on_saved_method' ) ) { + $default_fields['card-cvc-field'] = $cvc_field; + } + + $fields = wp_parse_args( $fields, apply_filters( 'woocommerce_credit_card_form_fields', $default_fields, $this->id ) ); + ?> + +
+ id ); ?> + + id ); ?> +
+
+ supports( 'credit_card_form_cvc_on_saved_method' ) ) { + echo '
' . $cvc_field . '
'; + } + } + +} diff --git a/includes/gateways/class-wc-payment-gateway-echeck.php b/includes/gateways/class-wc-payment-gateway-echeck.php new file mode 100644 index 00000000000..a5ab6b446e8 --- /dev/null +++ b/includes/gateways/class-wc-payment-gateway-echeck.php @@ -0,0 +1,72 @@ +supports( 'tokenization' ) && is_checkout(); + + if ( $display_tokenization ) { + $this->tokenization_script(); + if ( is_user_logged_in() ) { + $this->saved_payment_methods(); + } + $this->use_new_payment_method_checkbox(); + } + + $this->form(); + + if ( $display_tokenization ) { + $this->save_payment_method_checkbox(); + } + } + + /** + * Outputs fields for entering eCheck information. + * @since 2.6.0 + */ + public function form() { + $html = ''; + $fields = array(); + + $default_fields = array( + 'routing-number' => '

+ + +

', + 'account-number' => '

+ + +

', + ); + + $fields = wp_parse_args( $fields, apply_filters( 'woocommerce_echeck_form_fields', $default_fields, $this->id ) ); + ?> + +
+ id ); ?> + + id ); ?> +
+
h;h++)g+="
  • "+Simplify_commerce_params[e[h].field]+" "+Simplify_commerce_params.is_invalid+" - "+e[h].message+".
  • ";d.prepend('")}}else d.append(''),c.submit()}a(function(){a(document.body).on("checkout_error",function(){a(".simplify-token").remove()}),a("form.checkout").on("checkout_place_order_simplify_commerce",function(){return b()}),a("form#order_review").on("submit",function(){return b()}),a("form.checkout, form#order_review").on("change","#simplify_commerce-cc-form input",function(){a(".simplify-token").remove()})})}(jQuery); \ No newline at end of file +!function(a){function b(){var b=a("form.checkout, form#order_review, form#add_payment_method");if((a("#payment_method_simplify_commerce").is(":checked")&&a("#wc-simplify_commerce-new").is(":checked")||"1"===a("#woocommerce_add_payment_method").val())&&0===a("input.simplify-token").length){b.block({message:null,overlayCSS:{background:"#fff",opacity:.6}});var d=a("#simplify_commerce-card-number").val(),e=a("#simplify_commerce-card-cvc").val(),f=a.payment.cardExpiryVal(a("#simplify_commerce-card-expiry").val()),g=b.find("#billing_address_1").val(),h=b.find("#billing_address_2").val(),i=b.find("#billing_country").val(),j=b.find("#billing_state").val(),k=b.find("#billing_city").val(),l=b.find("#billing_postcode").val();return d=d.replace(/\s/g,""),SimplifyCommerce.generateToken({key:Simplify_commerce_params.key,card:{number:d,cvc:e,expMonth:f.month,expYear:f.year-2e3,addressLine1:g,addressLine2:h,addressCountry:i,addressState:j,addressZip:l,addressCity:k}},c),!1}return!0}function c(b){var c=a("form.checkout, form#order_review, form#add_payment_method"),d=a("#wc-simplify_commerce-cc-form");if(b.error){if(a(".woocommerce-error, .simplify-token",d).remove(),c.unblock(),"validation"===b.error.code){for(var e=b.error.fieldErrors,f=e.length,g="",h=0;f>h;h++)g+="
  • "+Simplify_commerce_params[e[h].field]+" "+Simplify_commerce_params.is_invalid+" - "+e[h].message+".
  • ";d.prepend('")}}else d.append(''),c.submit()}a(function(){a(document.body).on("checkout_error",function(){a(".simplify-token").remove()}),a("form.checkout").on("checkout_place_order_simplify_commerce",function(){return b()}),a("form#order_review").on("submit",function(){return b()}),a("form#add_payment_method").on("submit",function(){return b()}),a("form.checkout, form#order_review, form#add_payment_method").on("change","#wc-simplify_commerce-cc-form input",function(){a(".simplify-token").remove()})})}(jQuery); \ No newline at end of file diff --git a/includes/gateways/simplify-commerce/class-wc-gateway-simplify-commerce.php b/includes/gateways/simplify-commerce/class-wc-gateway-simplify-commerce.php index a3adada79b6..0e56d4cde43 100644 --- a/includes/gateways/simplify-commerce/class-wc-gateway-simplify-commerce.php +++ b/includes/gateways/simplify-commerce/class-wc-gateway-simplify-commerce.php @@ -8,13 +8,13 @@ if ( ! defined( 'ABSPATH' ) ) { * Simplify Commerce Gateway. * * @class WC_Gateway_Simplify_Commerce - * @extends WC_Payment_Gateway + * @extends WC_Payment_Gateway_CC * @since 2.2.0 * @version 1.0.0 * @package WooCommerce/Classes/Payment * @author WooThemes */ -class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { +class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway_CC { /** * Constructor. @@ -23,6 +23,7 @@ class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { $this->id = 'simplify_commerce'; $this->method_title = __( 'Simplify Commerce', 'woocommerce' ); $this->method_description = __( 'Take payments via Simplify Commerce - uses simplify.js to create card tokens and the Simplify Commerce SDK. Requires SSL when sandbox is disabled.', 'woocommerce' ); + $this->new_method_label = __( 'Use a new card', 'woocommerce' ); $this->has_fields = true; $this->supports = array( 'subscriptions', @@ -37,6 +38,7 @@ class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { 'subscription_date_changes', 'multiple_subscriptions', 'default_credit_card_form', + 'tokenization', 'refunds', 'pre-orders' ); @@ -274,7 +276,7 @@ class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { } if ( 'standard' == $this->mode ) { - $this->credit_card_form( array( 'fields_have_names' => false ) ); + parent::payment_fields(); } } @@ -282,7 +284,16 @@ class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { * Outputs scripts used for simplify payment. */ public function payment_scripts() { - if ( ! is_checkout() || ! $this->is_available() ) { + $load_scripts = false; + + if ( is_checkout() ) { + $load_scripts = true; + } + if ( $this->is_available() ) { + $load_scripts = true; + } + + if ( false === $load_scripts ) { return; } @@ -301,6 +312,98 @@ class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { ) ); } + public function add_payment_method() { + if ( empty ( $_POST['simplify_token'] ) ) { + wc_add_notice( __( 'There was a problem adding this card.', 'woocommerce' ), 'error' ); + return; + } + + $cart_token = wc_clean( $_POST['simplify_token'] ); + $customer_token = $this->get_users_token(); + $current_user = wp_get_current_user(); + $customer_info = array( + 'email' => $current_user->user_email, + 'name' => $current_user->display_name, + ); + + $token = $this->save_token( $customer_token, $cart_token, $customer_info ); + if ( is_null( $token ) ) { + wc_add_notice( __( 'There was a problem adding this card.', 'woocommerce' ), 'error' ); + return; + } + + return array( + 'result' => 'success', + 'redirect' => wc_get_endpoint_url( 'payment-methods' ), + ); + } + + /** + * Actualy saves a customer token to the database. + * + * @param WC_Payment_Token $customer_token Payment Token + * @param string $cart_token CC Token + * @param array $customer_info 'email', 'name' + */ + public function save_token( $customer_token, $cart_token, $customer_info ) { + if ( ! is_null( $customer_token ) ) { + $customer = Simplify_Customer::findCustomer( $customer_token->get_token() ); + $updates = array( 'token' => $cart_token ); + $customer->setAll( $updates ); + $customer->updateCustomer(); + $customer = Simplify_Customer::findCustomer( $customer_token->get_token() ); // get updated customer with new set card + $token = $customer_token; + } else { + $customer = Simplify_Customer::createCustomer( array( + 'token' => $cart_token, + 'email' => $customer_info['email'], + 'name' => $customer_info['name'], + ) ); + $token = new WC_Payment_Token_CC(); + $token->set_token( $customer->id ); + } + + // If we were able to create an save our card, save the data on our side too + if ( is_object( $customer ) && '' != $customer->id ) { + $customer_properties = $customer->getProperties(); + $card = $customer_properties['card']; + $token->set_gateway_id( $this->id ); + $token->set_card_type( strtolower( $card->type ) ); + $token->set_last4( $card->last4 ); + $expiry_month = ( 1 === strlen( $card->expMonth ) ? '0' . $card->expMonth : $card->expMonth ); + $token->set_expiry_month( $expiry_month ); + $token->set_expiry_year( '20' . $card->expYear ); + if ( is_user_logged_in() ) { + $token->set_user_id( get_current_user_id() ); + } + $token->save(); + return $token; + } + + return null; + } + + /** + * Process customer: updating or creating a new customer/saved CC + * + * @param WC_Order $order Order object + * @param WC_Payment_Token $customer_token Payment Token + * @param string $cart_token CC Token + */ + protected function process_customer( $order, $customer_token = null, $cart_token = '' ) { + // Are we saving a new payment method? + if ( is_user_logged_in() && isset( $_POST['wc-simplify_commerce-new-payment-method'] ) && true === (bool) $_POST['wc-simplify_commerce-new-payment-method'] ) { + $customer_info = array( + 'email' => $order->billing_email, + 'name' => trim( $order->get_formatted_billing_full_name() ), + ); + $token = $this->save_token( $customer_token, $cart_token, $customer_info ); + if ( ! is_null( $token ) ) { + $order->add_payment_token( $token ); + } + } + } + /** * Process standard payments. * @@ -310,10 +413,10 @@ class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { * @uses Simplify_BadRequestException * @return array */ - protected function process_standard_payments( $order, $cart_token = '' ) { + protected function process_standard_payments( $order, $cart_token = '', $customer_token = '' ) { try { - if ( empty( $cart_token ) ) { + if ( empty( $cart_token ) && empty( $customer_token ) ) { $error_msg = __( 'Please make sure your card details have been entered correctly and that your browser supports JavaScript.', 'woocommerce' ); if ( 'yes' == $this->sandbox ) { @@ -323,26 +426,44 @@ class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { throw new Simplify_ApiException( $error_msg ); } - $payment = Simplify_Payment::createPayment( array( - 'amount' => $order->order_total * 100, // In cents. - 'token' => $cart_token, - 'description' => sprintf( __( '%s - Order #%s', 'woocommerce' ), esc_html( get_bloginfo( 'name', 'display' ) ), $order->get_order_number() ), - 'currency' => strtoupper( get_woocommerce_currency() ), - 'reference' => $order->id - ) ); + // We need to figure out if we want to charge the card token (new unsaved token, no customer, etc) + // or the customer token (just saved method, previously saved method) + $pass_tokens = array(); - $order_complete = $this->process_order_status( $order, $payment->id, $payment->paymentStatus, $payment->authCode ); + if ( ! empty ( $cart_token ) ) { + $pass_tokens['token'] = $cart_token; + } + + if ( ! empty ( $customer_token ) ) { + $pass_tokens['customer'] = $customer_token; + // Use the customer token only, since we already saved the (one time use) card token to the customer + if ( isset( $_POST['wc-simplify_commerce-new-payment-method'] ) && true === (bool) $_POST['wc-simplify_commerce-new-payment-method'] ) { + unset( $pass_tokens['token'] ); + } + } + + // Did we create an account and save a payment method? We might need to use the customer token instead of the card token + if ( isset( $_POST['createaccount'] ) && true === (bool) $_POST['createaccount'] && empty ( $customer_token ) ) { + $user_token = $this->get_users_token(); + if ( ! is_null( $user_token ) ) { + $pass_tokens['customer'] = $user_token->get_token(); + unset( $pass_tokens['token'] ); + } + } + + $payment_response = $this->do_payment( $order, $order->get_total(), $pass_tokens ); + + if ( is_wp_error( $payment_response ) ) { + throw new Exception( $payment_response->get_error_message() ); + } else { + // Remove cart + WC()->cart->empty_cart(); - if ( $order_complete ) { // Return thank you page redirect return array( 'result' => 'success', 'redirect' => $this->get_return_url( $order ) ); - } else { - $order->add_order_note( __( 'Simplify payment declined', 'woocommerce' ) ); - - throw new Simplify_ApiException( __( 'Payment was declined - please try another card.', 'woocommerce' ) ); } } catch ( Simplify_ApiException $e ) { @@ -361,6 +482,62 @@ class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { } } + /** + * do payment function. + * + * @param WC_order $order + * @param int $amount (default: 0) + * @uses Simplify_BadRequestException + * @return bool|WP_Error + */ + public function do_payment( $order, $amount = 0, $token = array() ) { + if ( $amount * 100 < 50 ) { + return new WP_Error( 'simplify_error', __( 'Sorry, the minimum allowed order total is 0.50 to use this payment method.', 'woocommerce' ) ); + } + + try { + // Charge the customer + $data = array( + 'amount' => $amount * 100, // In cents. + 'description' => sprintf( __( '%s - Order #%s', 'woocommerce' ), esc_html( get_bloginfo( 'name', 'display' ) ), $order->get_order_number() ), + 'currency' => strtoupper( get_woocommerce_currency() ), + 'reference' => $order->id + ); + + $data = array_merge( $data, $token ); + $payment = Simplify_Payment::createPayment( $data ); + + } catch ( Exception $e ) { + + $error_message = $e->getMessage(); + + if ( $e instanceof Simplify_BadRequestException && $e->hasFieldErrors() && $e->getFieldErrors() ) { + $error_message = ''; + foreach ( $e->getFieldErrors() as $error ) { + $error_message .= ' ' . $error->getFieldName() . ': "' . $error->getMessage() . '" (' . $error->getErrorCode() . ')'; + } + } + + $order->add_order_note( sprintf( __( 'Simplify payment error: %s', 'woocommerce' ), $error_message ) ); + + return new WP_Error( 'simplify_payment_declined', $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + if ( 'APPROVED' == $payment->paymentStatus ) { + // Payment complete + $order->payment_complete( $payment->id ); + + // Add order note + $order->add_order_note( sprintf( __( 'Simplify payment approved (ID: %s, Auth Code: %s)', 'woocommerce' ), $payment->id, $payment->authCode ) ); + + return true; + } else { + $order->add_order_note( __( 'Simplify payment declined', 'woocommerce' ) ); + + return new WP_Error( 'simplify_payment_declined', __( 'Payment was declined - please try another card.', 'woocommerce' ) ); + } + } + /** * Process standard payments. * @@ -374,19 +551,52 @@ class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { ); } + protected function get_users_token() { + $customer_token = null; + if ( is_user_logged_in() ) { + $tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() ) ; + foreach ( $tokens as $token ) { + if ( $token->get_gateway_id() === $this->id ) { + $customer_token = $token; + break; + } + } + } + return $customer_token; + } + /** * Process the payment. * * @param int $order_id */ public function process_payment( $order_id ) { - $cart_token = isset( $_POST['simplify_token'] ) ? wc_clean( $_POST['simplify_token'] ) : ''; - $order = wc_get_order( $order_id ); + $order = wc_get_order( $order_id ); - if ( 'hosted' == $this->mode ) { + // Payment/CC form is hosted on Simplify + if ( 'hosted' === $this->mode ) { return $this->process_hosted_payments( $order ); - } else { - return $this->process_standard_payments( $order, $cart_token ); + } + + // New CC info was entered + if ( isset( $_POST['simplify_token'] ) ) { + $cart_token = wc_clean( $_POST['simplify_token'] ); + $customer_token = $this->get_users_token(); + $customer_token_value = ( ! is_null( $customer_token ) ? $customer_token->get_token() : '' ); + $this->process_customer( $order, $customer_token, $cart_token ); + return $this->process_standard_payments( $order, $cart_token, $customer_token_value ); + } + + // Possibly Create (or update) customer/save payment token, use an existing token, and then process the payment + if ( isset( $_POST['wc-simplify_commerce-payment-token'] ) && 'new' !== $_POST['wc-simplify_commerce-payment-token'] ) { + $token_id = wc_clean( $_POST['wc-simplify_commerce-payment-token'] ); + $token = WC_Payment_Tokens::get( $token_id ); + if ( $token->get_user_id() !== get_current_user_id() ) { + wc_add_notice( __( 'Please make sure your card details have been entered correctly and that your browser supports JavaScript.', 'woocommerce' ), 'error' ); + return; + } + $this->process_customer( $order, $token ); + return $this->process_standard_payments( $order, '', $token->get_token() ); } } @@ -411,7 +621,8 @@ class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway { 'address-city' => $order->billing_city, 'address-state' => $order->billing_state, 'address-zip' => $order->billing_postcode, - 'address-country' => $order->billing_country + 'address-country' => $order->billing_country, + 'operation' => 'create.token', ), $order->id ); return $args; diff --git a/includes/payment-tokens/class-wc-payment-token-cc.php b/includes/payment-tokens/class-wc-payment-token-cc.php new file mode 100644 index 00000000000..ca874345dc2 --- /dev/null +++ b/includes/payment-tokens/class-wc-payment-token-cc.php @@ -0,0 +1,139 @@ +meta['last4'] ) ) { + return false; + } + + if ( empty( $this->meta['expiry_year'] ) ) { + return false; + } + + if ( empty( $this->meta['expiry_month'] ) ) { + return false; + } + + if ( empty ( $this->meta['card_type'] ) ) { + return false; + } + + if ( 4 !== strlen( $this->meta['expiry_year'] ) ) { + return false; + } + + if ( 2 !== strlen( $this->meta['expiry_month'] ) ) { + return false; + } + + return true; + } + + /** + * Returns the card type (mastercard, visa, ...). + * @since 2.6.0 + * @return string Card type + */ + public function get_card_type() { + return isset( $this->meta['card_type'] ) ? $this->meta['card_type'] : null; + } + + /** + * Set the card type (mastercard, visa, ...). + * @since 2.6.0 + * @param string $type + */ + public function set_card_type( $type ) { + $this->meta['card_type'] = $type; + } + + /** + * Returns the card expiration year (YYYY). + * @since 2.6.0 + * @return string Expiration year + */ + public function get_expiry_year() { + return isset( $this->meta['expiry_year'] ) ? $this->meta['expiry_year'] : null; + } + + /** + * Set the expiration year for the card (YYYY format). + * @since 2.6.0 + * @param string $year + */ + public function set_expiry_year( $year ) { + $this->meta['expiry_year'] = $year; + } + + /** + * Returns the card expiration month (MM). + * @since 2.6.0 + * @return string Expiration month + */ + public function get_expiry_month() { + return isset( $this->meta['expiry_month'] ) ? $this->meta['expiry_month'] : null; + } + + /** + * Set the expiration month for the card (MM format). + * @since 2.6.0 + * @param string $month + */ + public function set_expiry_month( $month ) { + $this->meta['expiry_month'] = $month; + } + + /** + * Returns the last four digits. + * @since 2.6.0 + * @return string Last 4 digits + */ + public function get_last4() { + return isset( $this->meta['last4'] ) ? $this->meta['last4'] : null; + } + + /** + * Set the last four digits. + * @since 2.6.0 + * @param string $last4 + */ + public function set_last4( $last4 ) { + $this->meta['last4'] = $last4; + } + +} diff --git a/includes/payment-tokens/class-wc-payment-token-echeck.php b/includes/payment-tokens/class-wc-payment-token-echeck.php new file mode 100644 index 00000000000..3b42a197e6e --- /dev/null +++ b/includes/payment-tokens/class-wc-payment-token-echeck.php @@ -0,0 +1,61 @@ +meta['last4'] ) ) { + return false; + } + return true; + } + + /** + * Returns the last four digits. + * @since 2.6.0 + * @return string Last 4 digits + */ + public function get_last4() { + return isset( $this->meta['last4'] ) ? $this->meta['last4'] : null; + } + + /** + * Set the last four digits. + * @since 2.6.0 + * @param string $last4 + */ + public function set_last4( $last4 ) { + $this->meta['last4'] = $last4; + } + +} diff --git a/includes/shortcodes/class-wc-shortcode-my-account.php b/includes/shortcodes/class-wc-shortcode-my-account.php index e0a7ccd0e02..894ba97a885 100644 --- a/includes/shortcodes/class-wc-shortcode-my-account.php +++ b/includes/shortcodes/class-wc-shortcode-my-account.php @@ -344,4 +344,69 @@ class WC_Shortcode_My_Account { } } + + /** + * Deletes a payment method from a users list and displays a message to the user + * + * @since 2.6 + * @param int $id Payment Token ID + */ + public static function delete_payment_method( $id ) { + $token = WC_Payment_Tokens::get( $id ); + + if ( is_null( $token ) ) { + wc_add_notice( __( 'Invalid payment method', 'woocommerce' ), 'error' ); + woocommerce_account_payment_methods(); + return false; + } + + if ( get_current_user_id() !== $token->get_user_id() ) { + wc_add_notice( __( 'Invalid payment method', 'woocommerce' ), 'error' ); + woocommerce_account_payment_methods(); + return false; + } + + if ( false === wp_verify_nonce( $_REQUEST['_wpnonce'], 'delete-payment-method-' . $id ) ) { + wc_add_notice( __( 'Invalid payment method', 'woocommerce' ), 'error' ); + woocommerce_account_payment_methods(); + return false; + } + + WC_Payment_Tokens::delete( $id ); + wc_add_notice( __( 'Payment method deleted.', 'woocommerce' ) ); + woocommerce_account_payment_methods(); + } + + /** + * Sets a payment method as default and displays a message to the user + * + * @since 2.6 + * @param int $id Payment Token ID + */ + public static function set_default_payment_method( $id ) { + $token = WC_Payment_Tokens::get( $id ); + + if ( is_null( $token ) ) { + wc_add_notice( __( 'Invalid payment method', 'woocommerce' ), 'error' ); + woocommerce_account_payment_methods(); + return false; + } + + if ( get_current_user_id() !== $token->get_user_id() ) { + wc_add_notice( __( 'Invalid payment method', 'woocommerce' ), 'error' ); + woocommerce_account_payment_methods(); + return false; + } + + if ( false === wp_verify_nonce( $_REQUEST['_wpnonce'], 'set-default-payment-method-' . $id ) ) { + wc_add_notice( __( 'Invalid payment method', 'woocommerce' ), 'error' ); + woocommerce_account_payment_methods(); + return false; + } + + WC_Payment_Tokens::set_users_default( $token->get_user_id(), intval( $id ) ); + wc_add_notice( __( 'This payment method was successfully set as your default.', 'woocommerce' ) ); + woocommerce_account_payment_methods(); + } + } diff --git a/includes/wc-account-functions.php b/includes/wc-account-functions.php index a29ca1fb9eb..73a97685c51 100644 --- a/includes/wc-account-functions.php +++ b/includes/wc-account-functions.php @@ -208,3 +208,106 @@ function wc_get_account_payment_methods_columns() { 'actions' => ' ', ) ); } + +/** + * Get My Account > Payment methods types + * + * @since 2.6.0 + * @return array + */ +function wc_get_account_payment_methods_types() { + return apply_filters( 'woocommerce_payment_methods_types', array( + 'cc' => __( 'Credit Card', 'woocommerce' ), + 'echeck' => __( 'eCheck', 'woocommerce' ), + ) ); +} + +/** + * Returns an array of a user's saved payments list for output on the account tab. + * + * @since 2.6 + * @param array $list List of payment methods passed from wc_get_customer_saved_methods_list() + * @param int $customer_id The customer to fetch payment methods for + * @return array Filtered list of customers payment methods + */ +function wc_get_account_saved_payment_methods_list( $list, $customer_id ) { + $payment_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id ); + foreach ( $payment_tokens as $payment_token ) { + $delete_url = wc_get_endpoint_url( 'delete-payment-method', $payment_token->get_id() ); + $delete_url = wp_nonce_url( $delete_url, 'delete-payment-method-' . $payment_token->get_id() ); + $set_default_url = wc_get_endpoint_url( 'set-default-payment-method', $payment_token->get_id() ); + $set_default_url = wp_nonce_url( $set_default_url, 'set-default-payment-method-' . $payment_token->get_id() ); + + $type = strtolower( $payment_token->get_type() ); + $list[ $type ][] = array( + 'method' => array( + 'gateway' => $payment_token->get_gateway_id(), + ), + 'expires' => esc_html__( 'N/A', 'woocommerce' ), + 'is_default' => $payment_token->is_default(), + 'actions' => array( + 'delete' => array( + 'url' => $delete_url, + 'name' => esc_html__( 'Delete', 'woocommerce' ), + ), + ), + ); + $key = key( array_slice( $list[ $type ], -1, 1, true ) ); + + if ( ! $payment_token->is_default() ) { + $list[ $type ][$key]['actions']['default'] = array( + 'url' => $set_default_url, + 'name' => esc_html__( 'Make Default', 'woocommerce' ), + ); + } + + $list[ $type ][ $key ] = apply_filters( 'woocommerce_payment_methods_list_item', $list[ $type ][ $key ], $payment_token ); + } + return $list; +} + +add_filter( 'woocommerce_saved_payment_methods_list', 'wc_get_account_saved_payment_methods_list', 10, 2 ); + +/** + * Controls the output for credit cards on the my account page. + * + * @since 2.6 + * @param array $item Individual list item from woocommerce_saved_payment_methods_list + * @param WC_Payment_Token $payment_token The payment token associated with this method entry + * @return array Filtered item + */ +function wc_get_account_saved_payment_methods_list_item_cc( $item, $payment_token ) { + if ( 'cc' !== strtolower( $payment_token->get_type() ) ) { + return $item; + } + + $card_type = $payment_token->get_card_type(); + $item['method']['last4'] = $payment_token->get_last4(); + $item['method']['brand'] = ( ! empty( $card_type ) ? ucfirst( $card_type ) : esc_html__( 'Credit Card', 'woocommerce' ) ); + $item['expires'] = $payment_token->get_expiry_month() . '/' . substr( $payment_token->get_expiry_year(), -2 ); + + return $item; +} + +add_filter( 'woocommerce_payment_methods_list_item', 'wc_get_account_saved_payment_methods_list_item_cc', 10, 2 ); + +/** + * Controls the output for eChecks on the my account page. + * + * @since 2.6 + * @param array $item Individual list item from woocommerce_saved_payment_methods_list + * @param WC_Payment_Token $payment_token The payment token associated with this method entry + * @return array Filtered item + */ +function wc_get_account_saved_payment_methods_list_item_echeck( $item, $payment_token ) { + if ( 'echeck' !== strtolower( $payment_token->get_type() ) ) { + return $item; + } + + $item['method']['last4'] = $payment_token->get_last4(); + $item['method']['brand'] = esc_html__( 'eCheck', 'woocommerce' ); + + return $item; +} + +add_filter( 'woocommerce_payment_methods_list_item', 'wc_get_account_saved_payment_methods_list_item_echeck', 10, 2 ); diff --git a/includes/wc-core-functions.php b/includes/wc-core-functions.php index 084f64a9e0e..d5472ea4e79 100644 --- a/includes/wc-core-functions.php +++ b/includes/wc-core-functions.php @@ -936,6 +936,31 @@ function wc_get_shipping_zone( $package ) { return WC_Shipping_Zones::get_zone_matching_package( $package ); } +/** + * Get a nice name for credit card providers. + * + * @since 2.6.0 + * @param string $type Provider Slug/Type + * @return string + */ +function wc_get_credit_card_type_label( $type ) { + // Normalize + $type = strtolower( $type ); + $type = str_replace( '-', ' ', $type ); + $type = str_replace( '_', ' ', $type ); + + $labels = apply_filters( 'wocommerce_credit_card_type_labels', array( + 'mastercard' => __( 'MasterCard', 'woocommerce' ), + 'visa' => __( 'Visa', 'woocommerce' ), + 'discover' => __( 'Discover', 'woocommerce' ), + 'american express' => __( 'American Express', 'woocommerce' ), + 'diners' => __( 'Diners', 'woocommerce' ), + 'jcb' => __( 'JCB', 'woocommerce' ), + ) ); + + return apply_filters( 'woocommerce_get_credit_card_type_label', ( array_key_exists( $type, $labels ) ? $labels[ $type ] : ucfirst( $type ) ) ); +} + /** * Outputs a "back" link so admin screens can easily jump back a page. * diff --git a/includes/wc-template-hooks.php b/includes/wc-template-hooks.php index 409952bc043..7ee27c013d7 100644 --- a/includes/wc-template-hooks.php +++ b/includes/wc-template-hooks.php @@ -250,3 +250,5 @@ add_action( 'woocommerce_account_edit-address_endpoint', 'woocommerce_account_ed add_action( 'woocommerce_account_payment-methods_endpoint', 'woocommerce_account_payment_methods' ); add_action( 'woocommerce_account_add-payment-method_endpoint', 'woocommerce_account_add_payment_method' ); add_action( 'woocommerce_account_edit-account_endpoint', 'woocommerce_account_edit_account' ); +add_action( 'woocommerce_account_set-default-payment-method_endpoint', array( 'WC_Shortcode_My_Account', 'set_default_payment_method' ) ); +add_action( 'woocommerce_account_delete-payment-method_endpoint', array( 'WC_Shortcode_My_Account', 'delete_payment_method' ) ); diff --git a/templates/myaccount/form-add-payment-method.php b/templates/myaccount/form-add-payment-method.php index acfd3b114f1..af8580f8c5e 100644 --- a/templates/myaccount/form-add-payment-method.php +++ b/templates/myaccount/form-add-payment-method.php @@ -54,7 +54,7 @@ wc_get_template( 'myaccount/navigation.php' ); ?>
    - +
    diff --git a/templates/myaccount/payment-methods.php b/templates/myaccount/payment-methods.php index 6b005b561ac..dd955e882a8 100644 --- a/templates/myaccount/payment-methods.php +++ b/templates/myaccount/payment-methods.php @@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) { $saved_methods = wc_get_customer_saved_methods_list( get_current_user_id() ); $has_methods = (bool) $saved_methods; - +$types = wc_get_account_payment_methods_types(); wc_print_notices(); ?> @@ -43,27 +43,43 @@ wc_print_notices(); ?> - - - $column_name ) : ?> - - - - - + $methods ) : ?> + + + $column_name ) : ?> + + $action ) { + echo '' . esc_html( $action['name'] ) . ' '; + } + } + ?> + + + + -

    +

    - + diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 89667360371..83521aba1a7 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -91,6 +91,7 @@ class WC_Unit_Tests_Bootstrap { // framework require_once( $this->tests_dir . '/framework/class-wc-unit-test-factory.php' ); require_once( $this->tests_dir . '/framework/class-wc-mock-session-handler.php' ); + require_once( $this->tests_dir . '/framework/class-wc-payment-token-stub.php' ); // test cases require_once( $this->tests_dir . '/framework/class-wc-unit-test-case.php' ); @@ -104,6 +105,7 @@ class WC_Unit_Tests_Bootstrap { require_once( $this->tests_dir . '/framework/helpers/class-wc-helper-customer.php' ); require_once( $this->tests_dir . '/framework/helpers/class-wc-helper-order.php' ); require_once( $this->tests_dir . '/framework/helpers/class-wc-helper-shipping-zones.php' ); + require_once( $this->tests_dir . '/framework/helpers/class-wc-helper-payment-token.php' ); } /** diff --git a/tests/framework/class-wc-payment-token-stub.php b/tests/framework/class-wc-payment-token-stub.php new file mode 100644 index 00000000000..a24e2d7fa67 --- /dev/null +++ b/tests/framework/class-wc-payment-token-stub.php @@ -0,0 +1,26 @@ +meta['extra'] ) ? $this->meta['extra'] : ''; + } + + /** + * Set meta + * @param string $extra + */ + public function set_extra( $extra ) { + $this->meta['extra'] = $extra; + } +} diff --git a/tests/framework/helpers/class-wc-helper-payment-token.php b/tests/framework/helpers/class-wc-helper-payment-token.php new file mode 100644 index 00000000000..44cedf922e2 --- /dev/null +++ b/tests/framework/helpers/class-wc-helper-payment-token.php @@ -0,0 +1,56 @@ +set_last4( 1234 ); + $token->set_expiry_month( '08' ); + $token->set_expiry_year( '2016' ); + $token->set_card_type( 'visa' ); + $token->set_token( time() ); + $token->save(); + return $token; + } + + /** + * Create a new eCheck payment token + * + * @since 2.6 + * @return WC_Payment_Token_eCheck object + */ + public static function create_eCheck_token() { + $token = new WC_Payment_Token_eCheck(); + $token->set_last4( 1234 ); + $token->set_token( time() ); + $token->save(); + return $token; + } + + /** + * Create a new 'stub' payment token + * + * @since 2.6 + * @param string $extra A string to insert and get to test the metadata functionality of a token + * @return WC_Payment_Token_Stub object + */ + public static function create_stub_token( $extra ) { + $token = new WC_Payment_Token_Stub(); + $token->set_extra( $extra ); + $token->set_token( time() ); + $token->save(); + return $token; + } + +} + diff --git a/tests/unit-tests/order/functions.php b/tests/unit-tests/order/functions.php index 9d62232d0ff..46f689c41ac 100644 --- a/tests/unit-tests/order/functions.php +++ b/tests/unit-tests/order/functions.php @@ -123,4 +123,36 @@ class Functions extends \WC_Unit_Test_Case { // Assert the return when $the_order args is a random (incorrect) id. $this->assertFalse( wc_get_order( 123456 ) ); } + + /** + * Test getting an orders payment tokens + * + * @since 2.6 + */ + public function test_wc_order_get_payment_tokens() { + $order = \WC_Helper_Order::create_order(); + $this->assertEmpty( $order->get_payment_tokens() ); + + $token = \WC_Helper_Payment_Token::create_cc_token(); + update_post_meta( $order->id, '_payment_tokens', array( $token->get_id() ) ); + + $this->assertCount( 1, $order->get_payment_tokens() ); + } + + + /** + * Test adding a payment token to an order + * + * @since 2.6 + */ + public function test_wc_order_add_payment_token() { + $order = \WC_Helper_Order::create_order(); + $this->assertEmpty( $order->get_payment_tokens() ); + + $token = \WC_Helper_Payment_Token::create_cc_token(); + $order->add_payment_token( $token ); + + $this->assertCount( 1, $order->get_payment_tokens() ); + } + } diff --git a/tests/unit-tests/payment-tokens/cc.php b/tests/unit-tests/payment-tokens/cc.php new file mode 100644 index 00000000000..494b0e88dcf --- /dev/null +++ b/tests/unit-tests/payment-tokens/cc.php @@ -0,0 +1,138 @@ +set_token( time() . ' ' . __FUNCTION__ ); + $this->assertFalse( $token->validate() ); + $token->set_last4( '1111' ); + $token->set_expiry_year( '2016' ); + $token->set_expiry_month( '08' ); + $token->set_card_type( 'visa' ); + $this->assertTrue( $token->validate() ); + } + + /** + * Test validation for expiry length. + * @since 2.6.0 + */ + function test_wc_payment_token_cc_validate_expiry_length() { + $token = new \WC_Payment_Token_CC( 1 ); + $token->set_token( time() . ' ' . __FUNCTION__ ); + $this->assertFalse( $token->validate() ); + + $token->set_last4( '1111' ); + $token->set_expiry_year( '16' ); + $token->set_expiry_month( '08' ); + $token->set_card_type( 'visa' ); + + $this->assertFalse( $token->validate() ); + + $token->set_expiry_year( '2016' ); + $this->assertTrue( $token->validate() ); + + $token->set_expiry_month( '8' ); + $this->assertFalse( $token->validate() ); + } + + /** + * Test getting a card type. + * @since 2.6.0 + */ + public function test_wc_payment_token_cc_get_card_type() { + $token = new \WC_Payment_Token_CC( 1, array(), array( 'card_type' => 'mastercard' ) ); + $this->assertEquals( 'mastercard', $token->get_card_type() ); + } + + /** + * Test setting a token's card type. + * @since 2.6.0 + */ + public function test_wc_payment_token_cc_set_card_type() { + $token = new \WC_Payment_Token_CC( 1 ); + $token->set_card_type( 'visa' ); + $this->assertEquals( 'visa', $token->get_card_type() ); + } + + /** + * Test getting expiry year. + * @since 2.6.0 + */ + public function test_wc_payment_token_cc_get_expiry_year() { + $token = new \WC_Payment_Token_CC( 1, array(), array( 'expiry_year' => '2016' ) ); + $this->assertEquals( '2016', $token->get_expiry_year() ); + } + + /** + * Test setting a token's expiry year. + * @since 2.6.0 + */ + public function test_wc_payment_token_cc_set_expiry_year() { + $token = new \WC_Payment_Token_CC( 1 ); + $token->set_expiry_year( '2016' ); + $this->assertEquals( '2016', $token->get_expiry_year() ); + } + + /** + * Test getting expiry month. + * @since 2.6.0 + */ + public function test_wc_payment_token_cc_get_expiry_month() { + $token = new \WC_Payment_Token_CC( 1, array(), array( 'expiry_month' => '08' ) ); + $this->assertEquals( '08', $token->get_expiry_month() ); + } + + /** + * Test setting a token's expiry month. + * @since 2.6.0 + */ + public function test_wc_payment_token_cc_set_expiry_month() { + $token = new \WC_Payment_Token_CC( 1 ); + $token->set_expiry_month( '08' ); + $this->assertEquals( '08', $token->get_expiry_month() ); + } + + /** + * Test getting last4. + * @since 2.6.0 + */ + public function test_wc_payment_token_cc_get_last4() { + $token = new \WC_Payment_Token_CC( 1, array(), array( 'last4' => '1111' ) ); + $this->assertEquals( '1111', $token->get_last4() ); + } + + /** + * Test setting a token's last4. + * @since 2.6.0 + */ + public function test_wc_payment_token_cc_set_last4() { + $token = new \WC_Payment_Token_CC( 1 ); + $token->set_last4( '2222' ); + $this->assertEquals( '2222', $token->get_last4() ); + } + + /** + * Test reading/getting a token from DB correctly sets meta. + * @since 2.6.0 + */ + public function test_wc_payment_token_cc_read_pulls_meta() { + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token_id = $token->get_id(); + + $token_read = new \WC_Payment_Token_CC(); + $token_read->read( $token_id ); + + $this->assertEquals( '1234', $token_read->get_last4() ); + } + +} diff --git a/tests/unit-tests/payment-tokens/echeck.php b/tests/unit-tests/payment-tokens/echeck.php new file mode 100644 index 00000000000..3d8d0ac6ab1 --- /dev/null +++ b/tests/unit-tests/payment-tokens/echeck.php @@ -0,0 +1,55 @@ +set_token( time() . ' ' . __FUNCTION__ ); + $this->assertFalse( $token->validate() ); + $token->set_last4( '1111' ); + $this->assertTrue( $token->validate() ); + } + + /** + * Test getting last4. + * @since 2.6.0 + */ + public function test_wc_payment_token_echeck_get_last4() { + $token = new \WC_Payment_Token_eCheck( 1, array(), array( 'last4' => '1111' ) ); + $this->assertEquals( '1111', $token->get_last4() ); + } + + /** + * Test setting a token's last4. + * @since 2.6.0 + */ + public function test_wc_payment_token_echeck_set_last4() { + $token = new \WC_Payment_Token_eCheck( 1 ); + $token->set_last4( '2222' ); + $this->assertEquals( '2222', $token->get_last4() ); + } + + /** + * Test reading/getting a token from DB correctly sets meta. + * @since 2.6.0 + */ + public function test_wc_payment_token_echeck_read_pulls_meta() { + $token = \WC_Helper_Payment_Token::create_eCheck_token(); + $token_id = $token->get_id(); + + $token_read = new \WC_Payment_Token_eCheck(); + $token_read->read( $token_id ); + + $this->assertEquals( '1234', $token_read->get_last4() ); + } + +} diff --git a/tests/unit-tests/payment-tokens/payment-token.php b/tests/unit-tests/payment-tokens/payment-token.php new file mode 100644 index 00000000000..44c61013c78 --- /dev/null +++ b/tests/unit-tests/payment-tokens/payment-token.php @@ -0,0 +1,216 @@ +assertEquals( 1, $token->get_id() ); + } + + /** + * Test get type returns the class name/type. + * @since 2.6.0 + */ + public function test_wc_payment_token_get_type() { + $token = new \WC_Payment_Token_Stub( 1 ); + $this->assertEquals( 'stub', $token->get_type() ); + } + + /** + * Test get token to make sure it returns the passed token. + * @since 2.6.0 + */ + public function test_wc_payment_token_get_token() { + $raw_token = time() . ' ' . __FUNCTION__; + $token = new \WC_Payment_Token_Stub( 1, array( 'token' => $raw_token ) ); + $this->assertEquals( $raw_token, $token->get_token() ); + } + + /** + * Test set token to make sure it sets the pased token. + * @since 2.6.0 + */ + public function test_wc_payment_token_set_token() { + $raw_token = time() . ' ' . __FUNCTION__; + $token = new \WC_Payment_Token_Stub( 1 ); + $token->set_token( $raw_token ); + $this->assertEquals( $raw_token, $token->get_token() ); + } + + /** + * Test get user ID to make sure it passes the correct ID. + * @since 2.6.0 + */ + public function test_wc_payment_get_user_id() { + $token = new \WC_Payment_Token_Stub( 1, array( 'user_id' => 1 ) ); + $this->assertEquals( 1, $token->get_user_id() ); + } + + /** + * Test get user ID to make sure it returns 0 if there is no user ID. + * @since 2.6.0 + */ + public function test_wc_payment_get_user_id_defaults_to_0() { + $token = new \WC_Payment_Token_Stub( 1 ); + $this->assertEquals( 0, $token->get_user_id() ); + } + + /** + * Test set user ID to make sure it passes the correct ID. + * @since 2.6.0 + */ + public function test_wc_payment_set_user_id() { + $token = new \WC_Payment_Token_Stub( 1 ); + $token->set_user_id( 5 ); + $this->assertEquals( 5, $token->get_user_id() ); + } + + /** + * Test getting the gateway ID. + * @since 2.6.0 + */ + public function test_wc_payment_get_gateway_id() { + $token = new \WC_Payment_Token_Stub( 1, array( 'gateway_id' => 'paypal' ) ); + $this->assertEquals( 'paypal', $token->get_gateway_id() ); + } + + /** + * Test set the gateway ID. + * @since 2.6.0 + */ + public function test_wc_payment_set_gateway_id() { + $token = new \WC_Payment_Token_Stub( 1 ); + $token->set_gateway_id( 'paypal' ); + $this->assertEquals( 'paypal', $token->get_gateway_id() ); + } + + /** + * Test setting a token as default. + * @since 2.6.0 + */ + public function test_wc_payment_token_set_default() { + $token = new \WC_Payment_Token_Stub( 1 ); + $token->set_default( true ); + $this->assertTrue( $token->is_default() ); + $token->set_default( false ); + $this->assertFalse( $token->is_default() ); + } + + /** + * Test is_default. + * @since 2.6.0 + */ + public function test_wc_payment_token_is_default_returns_correct_state() { + $token = new \WC_Payment_Token_Stub( 1, array( 'is_default' => true ) ); + $this->assertTrue( $token->is_default() ); + $token = new \WC_Payment_Token_Stub( 1 ); + $this->assertFalse( $token->is_default() ); + $token = new \WC_Payment_Token_Stub( 1, array( 'is_default' => false ) ); + $this->assertFalse( $token->is_default() ); + } + + /** + * Test that get_data returns the correct internal representation for a token. + * @since 2.6.0 + */ + public function test_wc_payment_token_get_data() { + $raw_token = time() . ' ' . __FUNCTION__; + $token = new \WC_Payment_Token_Stub( 1, array( + 'token' => $raw_token, + 'gateway_id' => 'paypal' + ) ); + $token->set_extra( 'woocommerce' ); + + $data = $token->get_data(); + + $this->assertEquals( $raw_token, $data['token'] ); + $this->assertEquals( 'paypal', $data['gateway_id'] ); + $this->assertEquals( 'stub', $data['type'] ); + $this->assertEquals( 'woocommerce', $data['meta']['extra'] ); + } + + /** + * Test token validation. + * @since 2.6.0 + */ + public function test_wc_payment_token_validation() { + $token = new \WC_Payment_Token_Stub( 1 ); + $token->set_token( time() . ' ' . __FUNCTION__ ); + $this->assertTrue( $token->validate() ); + + $token = new \WC_Payment_Token_Stub( 1 ); + $this->assertFalse( $token->validate() ); + } + + /** + * Test reading a token from the database. + * @since 2.6.0 + */ + public function test_wc_payment_token_read() { + $token = \WC_Helper_Payment_Token::create_stub_token( __FUNCTION__ ); + $token_id = $token->get_id(); + + $token_read = new \WC_Payment_Token_Stub(); + $token_read->read( $token_id ); + + $this->assertEquals( $token->get_token(), $token_read->get_token() ); + $this->assertEquals( $token->get_extra(), $token_read->get_extra() ); + } + + /** + * Test updating a token. + * @since 2.6.0 + */ + public function test_wc_payment_token_update() { + $token = \WC_Helper_Payment_Token::create_stub_token( __FUNCTION__ ); + $this->assertEquals( __FUNCTION__, $token->get_extra() ); + $token->set_extra( ':)' ); + $token->update(); + $this->assertEquals( ':)', $token->get_extra() ); + } + + /** + * Test creating a new token. + * @since 2.6.0 + */ + public function test_wc_payment_token_create() { + $token = new \WC_Payment_Token_Stub(); + $token->set_extra( __FUNCTION__ ); + $token->set_token( time() ); + $token->create(); + + $this->assertNotEmpty( $token->get_id() ); + $this->assertEquals( __FUNCTION__, $token->get_extra() ); + } + + /** + * Test deleting a token. + * @since 2.6.0 + */ + public function test_wc_payment_token_delete() { + $token = \WC_Helper_Payment_Token::create_stub_token( __FUNCTION__ ); + $token_id = $token->get_id(); + $token->delete(); + $get_token = \WC_Payment_Tokens::get( $token_id ); + $this->assertNull( $get_token ); + } + + /** + * Test a meta function (like CC's last4) doesn't work on the core abstract class. + * @since 2.6.0 + */ + public function test_wc_payment_token_last4_doesnt_work() { + $token = new \WC_Payment_Token_Stub(); + $this->assertFalse( is_callable( $token, 'get_last4' ) ); + } + +} diff --git a/tests/unit-tests/payment-tokens/payment-tokens.php b/tests/unit-tests/payment-tokens/payment-tokens.php new file mode 100644 index 00000000000..8ceb1f30fe1 --- /dev/null +++ b/tests/unit-tests/payment-tokens/payment-tokens.php @@ -0,0 +1,177 @@ +assertEmpty( \WC_Payment_Tokens::get_order_tokens( $order->id ) ); + + $token = \WC_Helper_Payment_Token::create_cc_token(); + update_post_meta( $order->id, '_payment_tokens', array( $token->get_id() ) ); + + $this->assertCount( 1, \WC_Payment_Tokens::get_order_tokens( $order->id ) ); + + } + + /** + * Test getting tokens associated with a user and no gateway ID. + * @since 2.6.0 + */ + function test_wc_payment_tokens_get_customer_tokens_no_gateway() { + $this->assertEmpty( \WC_Payment_Tokens::get_customer_tokens( 1 ) ); + + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token->set_user_id( 1 ); + $token->save(); + + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token->set_user_id( 1 ); + $token->save(); + + $this->assertCount( 2, \WC_Payment_Tokens::get_customer_tokens( 1 ) ); + } + + /** + * Test getting tokens associated with a user and for a specific gateway. + * @since 2.6.0 + */ + function test_wc_payment_tokens_get_customer_tokens_with_gateway() { + $this->assertEmpty( \WC_Payment_Tokens::get_customer_tokens( 1 ) ); + + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token->set_user_id( 1 ); + $token->set_gateway_id( 'simplify_commerce' ); + $token->save(); + + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token->set_user_id( 1 ); + $token->set_gateway_id( 'paypal' ); + $token->save(); + + $this->assertCount( 2, \WC_Payment_Tokens::get_customer_tokens( 1 ) ); + $this->assertCount( 1, \WC_Payment_Tokens::get_customer_tokens( 1, 'simplify_commerce' ) ); + + foreach ( \WC_Payment_Tokens::get_customer_tokens( 1, 'simplify_commerce' ) as $simplify_token ) { + $this->assertEquals( 'simplify_commerce', $simplify_token->get_gateway_id() ); + } + } + + /** + * Test getting a customers default token. + * @since 2.6.0 + */ + function test_wc_get_customer_default_token() { + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token->set_user_id( 1 ); + $token->set_gateway_id( 'simplify_commerce' ); + $token->save(); + + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token->set_user_id( 1 ); + $token->set_default( true ); + $token->set_gateway_id( 'paypal' ); + $token->save(); + + $this->assertCount( 2, \WC_Payment_Tokens::get_customer_tokens( 1 ) ); + + $default_token = \WC_Payment_Tokens::get_customer_default_token( 1 ); + $this->assertEquals( 'paypal', $default_token->get_gateway_id() ); + } + + /** + * Test getting a customers default token, when there is no default token. + * @since 2.6.0 + */ + function test_wc_get_customer_default_token_returns_null_when_no_default_token() { + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token->set_user_id( 1 ); + $token->set_gateway_id( 'simplify_commerce' ); + $token->save(); + + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token->set_user_id( 1 ); + $token->set_gateway_id( 'paypal' ); + $token->save(); + + $this->assertCount( 2, \WC_Payment_Tokens::get_customer_tokens( 1 ) ); + + $default_token = \WC_Payment_Tokens::get_customer_default_token( 1 ); + $this->assertNull( $default_token ); + } + + /** + * Test getting a token by ID. + * @since 2.6.0 + */ + function test_wc_payment_tokens_get() { + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token_id = $token->get_id(); + $get_token = \WC_Payment_Tokens::get( $token_id ); + $this->assertEquals( $token->get_token(), $get_token->get_token() ); + } + + /** + * Test deleting a token by ID. + * @since 2.6.0 + */ + function test_wc_payment_tokens_delete() { + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token_id = $token->get_id(); + + \WC_Payment_Tokens::delete( $token_id ); + + $get_token = \WC_Payment_Tokens::get( $token_id ); + $this->assertNull( $get_token ); + } + + /** + * Test getting a token's type by ID. + * @since 2.6.0 + */ + function test_wc_payment_tokens_get_type_by_id() { + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token_id = $token->get_id(); + $this->assertEquals( 'CC', \WC_Payment_Tokens::get_token_type_by_id( $token_id ) ); + } + + /** + * Test setting a users default token. + * @since 2.6.0 + */ + function test_wc_payment_tokens_set_users_default() { + $token = \WC_Helper_Payment_Token::create_cc_token(); + $token_id = $token->get_id(); + $token->set_user_id( 1 ); + $token->save(); + + $token2 = \WC_Helper_Payment_Token::create_cc_token(); + $token_id_2 = $token2->get_id(); + $token2->set_user_id( 1 ); + $token2->save(); + + $this->assertFalse( $token->is_default() ); + $this->assertFalse( $token2->is_default() ); + + \WC_Payment_Tokens::set_users_default( 1, $token_id_2 ); + $token->read( $token_id ); + $token2->read( $token_id_2 ); + $this->assertFalse( $token->is_default() ); + $this->assertTrue( $token2->is_default() ); + + \WC_Payment_Tokens::set_users_default( 1, $token_id ); + $token->read( $token_id ); + $token2->read( $token_id_2 ); + $this->assertTrue( $token->is_default() ); + $this->assertFalse( $token2->is_default() ); + } + +} diff --git a/uninstall.php b/uninstall.php index 146f2b42285..13f66193b94 100644 --- a/uninstall.php +++ b/uninstall.php @@ -50,6 +50,8 @@ if ( ! empty( $status_options['uninstall_data'] ) ) { $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_shipping_zone_locations" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_shipping_zones" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_sessions" ); + $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_payment_tokens" ); + $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_payment_tokenmeta" ); // Delete options. $wpdb->query("DELETE FROM $wpdb->options WHERE option_name LIKE 'woocommerce\_%';"); diff --git a/woocommerce.php b/woocommerce.php index 37112aaad57..5d393eaf51c 100644 --- a/woocommerce.php +++ b/woocommerce.php @@ -167,6 +167,7 @@ final class WooCommerce { add_action( 'init', array( $this, 'init' ), 0 ); add_action( 'init', array( 'WC_Shortcodes', 'init' ) ); add_action( 'init', array( 'WC_Emails', 'init_transactional_emails' ) ); + add_action( 'init', array( $this, 'payment_token_metadata_wpdbfix' ), 0 ); } /** @@ -251,21 +252,25 @@ final class WooCommerce { include_once( 'includes/class-wc-tracker.php' ); } - $this->query = include( 'includes/class-wc-query.php' ); // The main query class - $this->api = include( 'includes/class-wc-api.php' ); // API Class + $this->query = include( 'includes/class-wc-query.php' ); // The main query class + $this->api = include( 'includes/class-wc-api.php' ); // API Class - include_once( 'includes/class-wc-auth.php' ); // Auth Class - include_once( 'includes/class-wc-post-types.php' ); // Registers post types - include_once( 'includes/abstracts/abstract-wc-product.php' ); // Products - include_once( 'includes/abstracts/abstract-wc-order.php' ); // Orders - include_once( 'includes/abstracts/abstract-wc-settings-api.php' ); // Settings API (for gateways, shipping, and integrations) - include_once( 'includes/abstracts/abstract-wc-shipping-method.php' ); // A Shipping method - include_once( 'includes/abstracts/abstract-wc-payment-gateway.php' ); // A Payment gateway - include_once( 'includes/abstracts/abstract-wc-integration.php' ); // An integration with a service - include_once( 'includes/class-wc-product-factory.php' ); // Product factory - include_once( 'includes/class-wc-countries.php' ); // Defines countries and states - include_once( 'includes/class-wc-integrations.php' ); // Loads integrations - include_once( 'includes/class-wc-cache-helper.php' ); // Cache Helper + include_once( 'includes/class-wc-auth.php' ); // Auth Class + include_once( 'includes/class-wc-post-types.php' ); // Registers post types + include_once( 'includes/abstracts/abstract-wc-payment-token.php' ); // Payment Tokens + include_once( 'includes/abstracts/abstract-wc-product.php' ); // Products + include_once( 'includes/abstracts/abstract-wc-order.php' ); // Orders + include_once( 'includes/abstracts/abstract-wc-settings-api.php' ); // Settings API (for gateways, shipping, and integrations) + include_once( 'includes/abstracts/abstract-wc-shipping-method.php' ); // A Shipping method + include_once( 'includes/abstracts/abstract-wc-payment-gateway.php' ); // A Payment gateway + include_once( 'includes/abstracts/abstract-wc-integration.php' ); // An integration with a service + include_once( 'includes/class-wc-product-factory.php' ); // Product factory + include_once( 'includes/class-wc-payment-tokens.php' ); // Payment tokens controller + include_once( 'includes/gateways/class-wc-payment-gateway-cc.php' ); // CC Payment Gateway + include_once( 'includes/gateways/class-wc-payment-gateway-echeck.php' ); // eCheck Payment Gateway + include_once( 'includes/class-wc-countries.php' ); // Defines countries and states + include_once( 'includes/class-wc-integrations.php' ); // Loads integrations + include_once( 'includes/class-wc-cache-helper.php' ); // Cache Helper if ( defined( 'WP_CLI' ) && WP_CLI ) { include_once( 'includes/class-wc-cli.php' ); @@ -466,6 +471,15 @@ final class WooCommerce { } } + /** + * WooCommerce Payment Token Meta API - set table name + */ + function payment_token_metadata_wpdbfix() { + global $wpdb; + $wpdb->payment_tokenmeta = $wpdb->prefix . 'woocommerce_payment_tokenmeta'; + $wpdb->tables[] = 'woocommerce_payment_tokenmeta'; + } + /** * Get Checkout Class. * @return WC_Checkout