I'm trying to add a custom payment gateway in my Woocommerce website. The flow will be like this:
After the user clicks on "Place order" button, he is redirected to the hosted payment page (HPP) of my payment processor. In order to achieve that, I must send some data (such as the merchant id, a hash, the order amount, etc.) through a hidden form which the payment processor needs to make the redirection to the HPP.
In the HPP, there is a form where the user can introduce his card data.
If everything is ok, the user is redirected to thank you page, otherwise he will be redirected to a fail page, or an error message shows up, I don't care.
This is what I have achieved so far:
When the user clicks on "Place order", a new order with the status "on hold" is created and he is redirected to a page called 'prueba.php'. This page only contains the hidden form with the data the payment processor needs to redirect the user to their gateway. This form is submitted automatically once the page is loaded (I have the feeling that this isn't the safest thing to do since if you open the element inspector, you can see the hidden inputs and their values, but I didn't know any other better ways to make this work). This is what I have in the main file of the plugin:
add_action( 'plugins_loaded', 'custom_init_gateway_class' );
function custom_init_gateway_class() {
class WC_My_Gateway extends WC_Payment_Gateway {
/**
* Constructor
*/
public function __construct() {
$this->id = 'mygateway';
$this->has_fields = true;
$this->method_title = 'My Gateway';
$this->method_description = 'Description of payment gateway';
$this->supports = array(
'products'
);
// Method
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option( 'title' );
$this->description = $this->get_option( 'description' );
$this->enabled = $this->get_option( 'enabled' );
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
}
/**
* Plugin options
*/
public function init_form_fields(){
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce' ),
'type' => 'checkbox',
'label' => __( 'Enable Card Payment', 'woocommerce' ),
'default' => 'yes'
),
'title' => array(
'title' => __( 'Title', 'woocommerce' ),
'type' => 'text',
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
'default' => __( 'Credit Payment', 'woocommerce' ),
'desc_tip' => true,
),
'description' => array(
'title' => __( 'Customer Message', 'woocommerce' ),
'type' => 'textarea',
'default' => ''
)
);
}
/*
* Processing the payments
*/
public function process_payment( $order_id ) {
global $woocommerce;
$order = new WC_Order( $order_id );
$order->update_status('on-hold', __( 'Awaiting card payment', 'woocommerce' ));
return array(
'result' => 'success',
'redirect' => 'https://mydomain.example/prueba.php?price='.$order->get_total().'¤cy='.$order->get_currency().'&order_id='.$order->get_id()
);
}
}
}
function custom_add_gateway_class( $methods ) {
$methods[] = 'WC_My_Gateway';
return $methods;
}
add_filter( 'woocommerce_payment_gateways', 'custom_add_gateway_class' );
The hidden form in "prueba.php". In the input "MERCHANT_RESPONSE_URL" I must specify which url I want the payment processor to send the data to, after the payment is completed:
<form method="POST" action="https://paymentprocessorgateway.example" name="respuesta" style="display: none;">
<input type="hidden" name="TIMESTAMP" value="<?php echo $timestamp; ?>">
<input type="hidden" name="MERCHANT_ID" value="12345">
<input type="hidden" name="AMOUNT" value="<?php echo $Amount; ?>">
<input type="hidden" name="CURRENCY" value="<?php echo $Currency; ?>">
<input type="hidden" name="SIGNATURE" value="<?php echo $hash; ?>">
<input type="hidden" name="COMMENT1" value="<?php echo $order_id; ?>">
<input type="hidden" name="MERCHANT_RESPONSE_URL" value="https://mydomain.example/response.php">
<input type="submit" value="Click here to buy">
</form>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script type="text/javascript">
function sendForm() {
$("form").submit();
}
sendForm();
</script>
After this form is sent, the user is redirected to the payment processor gateway where he can write and send his card data, and then the processor sends some POST data to "response.php". In this page, I compare a hash I receive from the payment processor with a hash I generate myself (with some of the other POST values received from the payment processor).
What I'd like to achieve is redirecting the user to the thank you page and change the order status to 'completed' if everything is correct, or showing an error message if it isn't. Also, I would like that the order is created here (if the payment is correct), not when the client clicks on 'Place order' button (I thought that knowing the id of the order, I will be able to change its status wherever I needed, or that I could create a new order anywhere, but I must be doing everything wrong since I don't know very well how woocommerce and OOP PHP works).
I tried writing this in "response.php", but the processor's gateway throws an error that says the transaction went well but it couldn't connect to the url specified in MERCHANT_RESPONSE_URL:
//Generating the hash with data received from processor
$respuesta = $_POST['TIMESTAMP'].".".$_POST['MERCHANT_ID'].".".$_POST['ORDER_ID'].".".$_POST['RESULT'];
$my_hash = sha1(sha1($respuesta);
global $woocommerce;
$order = wc_get_order( $order_id );
if ($my_hash != $_POST['HASH']) { //$_POST['HASH'] is the hash sent by the processor.
wc_add_notice( 'Please try again.', 'error' );
return;
} else {
$order->payment_complete();
$woocommerce->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $order )
);
}
If I delete the woocommerce related variables and use something like this to see if it's working, the alerts are printed correctly:
if ($my_hash != $_POST['HASH']) {
echo '<script>alert("Wrong transaction");</script>';
} else {
echo '<script>alert("Correct transaction");</script>';
}
It's like I can't use woocommerce related variables there. I guess I must attach this to some hook or something, but I don't know which or where, I have the feeling I have been making many mistakes along the way. If someone could throw some light on how to handle the response of the payment processor and integrate it with Woocommerce flow I would be eternally grateful.
First of all. If you're supposed to submit a hidden form, I wouldnt worry about security when it comes to the hidden or not aspect. Anything that is client side can always be viewed/tampered with anyway, so thats not where security measures should be taken imo.
Your last block of code mentions $old_hash, but it is empty? At least, your code block doesn't set it. What part of this code isn't working you say? Did you try logging to something to like an error log in it to see if it is called? (Like error_log('message'); )
You mentioned that HPP sends back a hash to a page of your choice, so I'd say response.php is probably the page where the user is sent after paying.
(Also, it is almost bed time for me, so apologies if I stop responding.)
Related
With the help of these 2 posts (WooCommerce editable custom checkout field and displayed in formatted address)( Add additional fields to Admin User under Customer shipping section in Woocommerce), I added the custom field in the checkout, my account, single order, admin, and email. but there are 3 more area I would like to show this custom field and I couldn't find the hook needed to show them. I searched online but with no luck at all. I would love it if someone could guide me on this.
1- under shipping address table in thank you page
2- under shipping address table in order view on my account page
3- I would like to show the field in the address on my account. but its only visible when I click edit
this is the code I used to add the extra field and its working so far
// HOUSE NUMBER EXTRA FIELD
// display shipping House Number in checkout and my account edit shipping address
add_filter( 'woocommerce_shipping_fields', 'add_shipping_house_number_field' );
function add_shipping_house_number_field( $fields ) {
$fields['shipping_house_number'] = array(
'label' => __( 'House Number', 'woocommerce' ),
//'required' => true,
'class' => array( 'form-row-first' ),
'priority' => 61,
);
return $fields;
}
// Display editable custom fields on admin single order pages (backend new order) edit pages inside edit shipping section
add_filter( 'woocommerce_admin_shipping_fields' , 'add_order_admin_edit_shipping_house_number' );
function add_order_admin_edit_shipping_house_number( $fields ) {
// Include shipping house_number as editable field
$fields['house_number'] = array( 'label' => __( 'House Number', 'woocommerce' ), 'show' => '0' );
return $fields;
}
// Load Ajax custom field data as customer billing/shipping address fields in backend new order (data will be pulled from the database)
add_filter( 'woocommerce_ajax_get_customer_details' , 'add_custom_fields_to_ajax_customer_details', 10, 3 );
function add_custom_fields_to_ajax_customer_details( $data, $customer, $user_id ) {
$data['shipping'][house_number] = $customer->get_meta('shipping_house_number');
return $data;
}
// Display reordered editable custom fields on admin single User pages
add_filter( 'woocommerce_customer_meta_fields', 'house_number_customer_meta_fields', 10, 1 );
function house_number_customer_meta_fields( $fields ) {
$fields['shipping']['fields']['shipping_house_number'] = array(
'label' => __( 'House Number', 'woocommerce' ),
);
return $fields;
}
// Adding custom placeholder to woocommerce formatted address only on Backend
add_filter( 'woocommerce_localisation_address_formats', 'admin_localisation_address_formats', 50, 1 );
function admin_localisation_address_formats( $address_formats ){
// Only in backend (Admin)
if( is_admin() || ! is_wc_endpoint_url() ) {
foreach( $address_formats as $country_code => $address_format ) {
$address_formats[$country_code] .= "\n{house_number}";
}
}
return $address_formats;
}
// Custom placeholder replacement to woocommerce formatted address
add_filter( 'woocommerce_formatted_address_replacements', 'custom_formatted_address_replacements', 10, 2 );
function custom_formatted_address_replacements( $replacements, $args ) {
$replacements['{house_number}'] = ! empty($args['house_number']) ? $args['house_number'] : '';
return $replacements;
}
// Add the shipping house number value to be displayed on email notifications under shipping address
add_filter( 'woocommerce_order_formatted_shipping_address', 'add_shipping_house_number_to_formatted_shipping_address', 100, 2 );
function add_shipping_house_number_to_formatted_shipping_address( $shipping_address, $order ) {
global $pagenow, $post_type;
// Not on admin order edit pages (as it's already displayed).
if( ! ( $pagenow === 'post.php' && $post_type === 'shop_order' && isset($_GET['action']) && $_GET['action'] === 'edit' ) ) {
// Include shipping phone on formatted shipping address
$shipping_address['house_number'] = $order->get_meta('_shipping_house_number');
}
return $shipping_address;
}
For thank you page and my account view order you can use WC woocommerce_order_get_formatted_shipping_address filter hook.
function add_woocommerce_order_get_formatted_shipping_address( $address, $empty_content, $raw_address, $order ){
$address .= '<br/> this is custom text';
return $address;
}
add_filter( 'woocommerce_order_get_formatted_shipping_address', 'add_woocommerce_order_get_formatted_shipping_address', 10, 4 );
Thank you Page
My Account View Order.
My Account edit address
You need to override. woocommerce\templates\myaccount\my-address.php
to your theme or plugin.
I have a custom payment gateway where I need to reload checkout page if payment failed.
Reason:
When Card details submitted the payment gateway generates a card token which I need to process the payment but we can use the card token only once with a request.
What I need:
Currently, I am just showing the error message and a return when payment is failed.
if($payment_status['status']){
$order->update_status( 'on-hold', __( "ABC Payment Done\n", 'stackoverflow' ) );
wc_reduce_stock_levels($order_id);
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $order )
);
}else{
wc_add_notice( $payment_status['message'] , 'error' );
return;
}
How can I reload/refresh the page if payment failed? S user can enter the card details again and we can process the payment.
Or any other suggestion?
It depends if you want it sync or async.
Sync way:
header("Refresh:0");
It refreshes your current page. You can specify also the page you want:
header("Refresh:0; url=another_page.php");
Async way:
Generate the content dynamically, clean all changes and tokens.
Fksjii is true.
However, it might be cleaner to have a specified page that generates token code.
With an XHR request you can get it, then submit every data with an XHR and wait for a callback.
However, if you can't edit JS code you might do
if ($failed) {
unset($_REQUEST);
Header("Refresh: 0");
exit;
}
Do not forget to clean $_GET or $_POST data before reloading your page.
It is always very important to exit even after a redirection sent in the headers. Indeed, headers are sent but code continue to execute and it could create some bugs.
You need return the array for example:
if ( $payment_status['status'] ) {
$order->update_status( 'on-hold', __( "ABC Payment Done\n", 'stackoverflow' ) );
wc_reduce_stock_levels($order_id);
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $order )
);
} else {
$order->update_status( 'failed', __( 'Payment error:', 'woocommerce' ) . $this->get_option( 'error_message' ) );
wc_add_notice( $payment_status['message'] , 'error' );
WC()->cart->empty_cart();
return array(
'result' => 'failure',
'redirect' => WC()->cart->get_checkout_url()
);
}
That code will redirect to the checkout page. Hope help you.
I have created a custom order status in my WooCommerce called Back Order (wc-backorder):
/**
* Add custom status to order list
*/
add_action( 'init', 'register_custom_post_status', 10 );
function register_custom_post_status() {
register_post_status( 'wc-backorder', array(
'label' => _x( 'Back Order', 'Order status', 'woocommerce' ),
'public' => true,
'exclude_from_search' => false,
'show_in_admin_all_list' => true,
'show_in_admin_status_list' => true,
'label_count' => _n_noop( 'Back Order <span class="count">(%s)</span>', 'Back Order <span class="count">(%s)</span>', 'woocommerce' )
) );
}
/**
* Add custom status to order page drop down
*/
add_filter( 'wc_order_statuses', 'custom_wc_order_statuses' );
function custom_wc_order_statuses( $order_statuses ) {
$order_statuses['wc-backorder'] = _x( 'Back Order', 'Order status', 'woocommerce' );
return $order_statuses;
}
Now I want to receive an email whenever an order is received that has been given the status quote. I've created a plugin based on this helpful article: How to Add a Custom WooCommerce Email
This link is containing my plugin source code and the functions.php code.
I added hook in function.php:
add_action( 'woocommerce_order_status_wc-order-confirmed', array( WC(), 'send_transactional_email' ), 10, 10 );
function so_27112461_woocommerce_email_actions( $actions ){
$actions[] = 'woocommerce_order_status_wc-order-confirmed';
return $actions;
}
add_filter( 'woocommerce_email_actions', 'so_27112461_woocommerce_email_actions' );
Nothing happens when an order changed into the 'Back Order' status.
Any ideas?
I've tried loads of different hooks but I can't seem to get the trigger function to run.
I am on latest versions of WordPress and WooCommerce (3.0+)
Thanks
- EDIT / UPDATE -
As the code tutorial you are using is really outdated (2013) for this new mega major version 3.0+, this custom function hooked in woocommerce_order_status_changed action hook will do the job. So You will be able to send a customized Processing email notification, when order status is changed to your custom status.
Here is that working and tested code for WC 3.0+:
add_action('woocommerce_order_status_changed', 'backorder_status_custom_notification', 10, 4);
function backorder_status_custom_notification( $order_id, $from_status, $to_status, $order ) {
if( $order->has_status( 'backorder' )) {
// Getting all WC_emails objects
$email_notifications = WC()->mailer()->get_emails();
// Customizing Heading and subject In the WC_email processing Order object
$email_notifications['WC_Email_Customer_Processing_Order']->heading = __('Your processing Back order','woocommerce');
$email_notifications['WC_Email_Customer_Processing_Order']->subject = 'Your {site_title} processing Back order receipt from {order_date}';
// Sending the customized email
$email_notifications['WC_Email_Customer_Processing_Order']->trigger( $order_id );
}
}
This code goes in function.php file of your active child theme (or theme) or also in any plugin file.
AS your custom status is wc-backorder, but not wc-order-confirmed, you just need to replace everywhere wc-order-confirmed by wc-backorder.
To make it work, you will have to change the 2 last hooked functions this way:
add_action( 'woocommerce_order_status_wc-backorder', array( WC(), 'send_transactional_email' ), 10, 1 );
add_filter( 'woocommerce_email_actions', 'filter_woocommerce_email_actions' );
function filter_woocommerce_email_actions( $actions ){
$actions[] = 'woocommerce_order_status_wc-backorder';
return $actions;
}
Code goes in function.php file of your active child theme (or theme) or also in any plugin file.
This should work (I can't test it as there is no the code of your custom plugin).
Reference source code: woocommerce_order_status_{$this->status_transition[to]} action hook
add_action("woocommerce_order_status_changed", "my_custom_notification");
function my_custom_notification($order_id, $checkout=null) {
global $woocommerce;
$order = new WC_Order( $order_id );
if($order->status === 'backorder' ) {
// Create a mailer
$mailer = $woocommerce->mailer();
$message_body = __( 'Hello world!!!' );
$message = $mailer->wrap_message(
// Message head and message body.
sprintf( __( 'Order %s received' ), $order->get_order_number() ), $message_body );
// Cliente email, email subject and message.
$mailer->send( $order->billing_email, sprintf( __( 'Order %s received' ), $order->get_order_number() ), $message );
}
}
Try this
I am trying to hide the payment method once user click place order using payment method lets say 'B' which returns in failure of payment.
public function process_payment( $order_id ) {
global $woocommerce;
$order = new WC_Order( $order_id );
if($this->api->somemethod()){
// Payment complete
$order->payment_complete();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $order )
);
}
else {
wc_add_notice( pll__('Payment error: You cannot use this payment method, Choose another method '), 'error' );
return;
}
}
I have used following filter
add_filter( 'woocommerce_payment_gateways', 'add_gateway_cb' );
But it only works on page reload. I want something which works with Ajax process payment or at least triggers the payment methods to reload upon failure.
Alternatively:
I can use custom JS on place order click.
I'm having issues getting my field to save using WordPresses setting API
I have 1 option page, with 1 section and 1 field. So it's pretty straight forward. Everything appearing/rendering fine, but I can't save the fields.
class LocalSEOAdmin {
var $slug = 'local_seo_settings';
var $name = "Local SEO";
public function __construct(){
// When WP calls admin_menu, call $this->admin_settings
if ( is_admin() ) {
add_action('admin_menu', array( &$this, 'add_options_page' ) ); // on admin_menu call $this->init_admin_menu
add_action( 'admin_init', array( &$this, 'add_setting_fields' ) );
}
}
public function add_options_page(){
add_options_page(
$this->name, // Page title
$this->name, // Menu Title
'manage_options', // Role
$this->slug, // Menu slug
array( &$this, 'local_seo_admin_menu_callback' ) // Callback to render the option page
);
}
public function local_seo_admin_menu_callback(){
?>
<div class='wrap'>
<h1><?php echo $this->name ?> Options</h1>
<form method='post' action='options.php'>
<?php
settings_fields( 'address_settings_section' );
do_settings_sections( $this->slug );
submit_button();
?>
</form>
</div>
<?php
}
public function add_setting_fields(){
add_settings_section(
'address_settings_section', // ID used to identify this section and with which to register options
'Address Options', // Title to be displayed on the administration page
array( &$this, '_render_address_options'), // Callback used to render the description of the section
$this->slug // Page on which to add this section of options
);
add_settings_field(
'address_line_1', // ID used to identify the field throughout the theme
'Address Line 1', // The label to the left of the option interface element
array( &$this, '_render_address_line_1'), // The name of the function responsible for rendering the option interface
$this->slug, // The page on which this option will be displayed
'address_settings_section', // The name of the section to which this field belongs
array( // The array of arguments to pass to the callback. In this case, just a description.
__( 'Activate this setting to display the header.', 'sandbox' ),
)
);
register_setting(
'address_settings_section',
'address_settings_section'
);
}
public function _render_address_options(){
echo '<p>Add you address</p>';
}
public function _render_address_line_1(){
$option = get_option( 'address_line_1' );
$html = '<input type="text" id="address_line_1" name="address_line_1" value="' . $option . '" />';
echo $html;
}
}
new LocalSEOAdmin;
I'm not sure what's going on, but I feel I've mixed up the required fields someone. The fields are showing in WordPress and when I hit update the "setting saved." messages comes up, but nothing gets saved.
there is just a little problem when you register your setting.
base on register_setting, the second parameter, is $option_name so you must put send name of you inputs
register_setting(
'address_settings_section',
'address_line_1'
);
and now it's worked.
you can find more about creating options pages in wordpress site.
In register_setting()
First paramerter is your option group name &
Second parameter is option name which is given in add_settings_field() , in your case is should be address_line_1 not address_settings_section
Finally
register_setting(
'address_settings_section',
'address_line_1'
)