WooCommerce Stripe Payment Gateway - Version 3.0.0

Version Description

  • First public WordPress.org release.
  • Refactor for WC 2.6 and above. Legacy support for 2.5.
  • Improved saved card handling using tokenization API in WooCommerce.

See changelog for all versions.

Download this release

Release Info

Developer mikejolley
Plugin Icon 128x128 WooCommerce Stripe Payment Gateway
Version 3.0.0
Comparing to
See all releases

Version 3.0.0

.github/CONTRIBUTING.md ADDED
@@ -0,0 +1,14 @@
1
+ ### Create Bug Reports
2
+
3
+ If you find a bug or suggest enhancement, let us know by [opening a new issue](https://github.com/woothemes/woocommerce-gateway-stripe/issues/new).
4
+
5
+ ### Write and submit a patch
6
+
7
+ If you'd like to fix a bug, you can submit a Pull Request. If possible, raises an issue first and link the issue in your [commit message](https://help.github.com/articles/closing-issues-via-commit-messages/) or [PR's body](https://github.com/blog/1506-closing-issues-via-pull-requests).
8
+
9
+ When creating Pull Requests, remember:
10
+
11
+ - [Check In Early, Check In Often](http://blog.codinghorror.com/check-in-early-check-in-often/).
12
+ - Write [good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
13
+ - Respect the [Best practices for WordPress development](http://jetpack.com/contribute/#practices).
14
+
.github/ISSUE_TEMPLATE.md ADDED
@@ -0,0 +1,28 @@
1
+ <!-- Thanks for contributing to this extension! Pick a clear title ("Order: Unable to refund") and proceed. -->
2
+
3
+ #### What I expected
4
+
5
+ <!-- What you expected when performing the steps -->
6
+
7
+ #### What happened instead
8
+
9
+ <!-- What actual results you got -->
10
+
11
+ #### Steps to reproduce the issue
12
+
13
+ <!-- Please add detailed steps to reproduce the issue. Make sure it's reproducible locally. Other extensions should be deactivated and standard theme is used when reproducing the issue locally -->
14
+
15
+ <!--
16
+ PLEASE NOTE
17
+ - These comments won't show up when you submit the issue.
18
+ - Everything is optional, but try to add as many details as possible.
19
+ - Screenshot worth a thousand words, use screenshots if possible.
20
+ - If requesting a new feature, explain why you'd like to see it added.
21
+ - Please apply appropriate labels on the issue
22
+ -->
23
+
24
+
25
+ -------------------
26
+
27
+ - [ ] Issue assigned to next milestone.
28
+ - [ ] Issue assigned a priority (will be assessed by maintainers).
.github/PULL_REQUEST_TEMPLATE.md ADDED
@@ -0,0 +1,9 @@
1
+ Fixes # .
2
+
3
+ #### Changes proposed in this Pull Request:
4
+ -
5
+
6
+ -------------------
7
+ - [ ] Make sure your changes respect [WordPress' coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/).
8
+ - [ ] Did you make changes, or create a **new .js file**? If **Gruntfile.js** exists in the repo, make sure to run `grunt`.
9
+
DEVELOPER.md ADDED
@@ -0,0 +1,46 @@
1
+ # DEVELOPER.md
2
+
3
+ ## Testing
4
+
5
+ * In wp-admin > WooCommerce > Settings > Checkout > Stripe, Enable Stripe, Enable Test Mode, Enable Stripe Checkout and Enable Payment via Saved Cards
6
+ * In wp-admin > WooCommerce > Settings > Checkout > Stripe, enter a Test Secret Key and a Test Publishable Key
7
+ * Enable at least one other payment gateway (e.g. Cheques)
8
+
9
+ * On the front side, place an item in your cart and proceed to Checkout
10
+ * Fill in all required fields in the Billing Details area
11
+ * Select Credit Card (Stripe) and "Use a new credit card"
12
+ * Click on Continue to payment
13
+ * Verify you get the stripe modal requesting card number, expiration and CVC
14
+ * Enter 4242 4242 4242 4242, 12/17, 123
15
+ * Leave Remember Me unchecked
16
+ * Click Confirm and Pay
17
+ * Verify the modal closes, the page dims for a bit, and then you are redirected to Order Received
18
+
19
+ * Repeat the above steps, but this time instead of "Use a new credit card" use a stored card
20
+ * Click on Continue to payment
21
+ * Verify the page dims for a bit and then you are redirected to Order Received
22
+
23
+ * Repeat the above steps, but this time clear the Billing Details (e.g. Name, etc)
24
+ * Choose a stored card in Stripe
25
+ * Click on Continue to payment
26
+ * Verify you get prompted to fill in required fields.
27
+ * Fill in the required fields
28
+ * Click on Continue to payment
29
+ * Verify the page dims for a bit and then you are redirected to Order Received
30
+
31
+ * Repeat the above steps, but this time choose the "Cheque Payment" gateway
32
+ * Click on Place Order
33
+ * Verify the page dims for a bit and then you are redirected to Order Received
34
+
35
+ * Repeat at least the "Use a new credit card" case on Chrome on an iPhone or iPad
36
+
37
+ * In wp-admin > WooCommerce > Settings > Checkout > Stripe, uncheck Enable Payment via Saved Cards
38
+ * On the front side, place an item in your cart and proceed to Checkout
39
+ * Fill in all required fields in the Billing Details area
40
+ * Select Credit Card (Stripe)
41
+ * Click on Continue to payment
42
+ * Verify you get the stripe modal requesting card number, expiration and CVC
43
+ * Enter 4242 4242 4242 4242, 12/17, 123
44
+ * Leave Remember Me unchecked
45
+ * Click Confirm and Pay
46
+ * Verify the modal closes, the page dims for a bit, and then you are redirected to Order Received
assets/images/bitcoin.png ADDED
Binary file
assets/images/bitcoin.svg ADDED
@@ -0,0 +1,7 @@
1
+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
2
+ <svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
3
+ <g transform="translate(0.00630876,-0.00301984)">
4
+ <path fill="#f7931a" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
5
+ <path fill="#FFF" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
6
+ </g>
7
+ </svg>
assets/images/diners.png ADDED
Binary file
assets/js/stripe.js ADDED
@@ -0,0 +1,136 @@
1
+ /* global wc_stripe_params */
2
+ Stripe.setPublishableKey( wc_stripe_params.key );
3
+
4
+ jQuery( function( $ ) {
5
+
6
+ /* Open and close for legacy class */
7
+ jQuery( "form.checkout, form#order_review" ).on('change', 'input[name="wc-stripe-payment-token"]', function() {
8
+ if ( jQuery( '.stripe-legacy-payment-fields input[name="wc-stripe-payment-token"]:checked' ).val() == 'new' ) {
9
+ jQuery( '.stripe-legacy-payment-fields #stripe-payment-data' ).slideDown( 200 );
10
+ } else {
11
+ jQuery( '.stripe-legacy-payment-fields #stripe-payment-data' ).slideUp( 200 );
12
+ }
13
+ } );
14
+
15
+ /**
16
+ * Object to handle Stripe payment forms.
17
+ */
18
+ var wc_stripe_form = {
19
+
20
+ /**
21
+ * Initialize event handlers and UI state.
22
+ */
23
+ init: function( form ) {
24
+ this.form = form;
25
+
26
+ $( this.form )
27
+ .on(
28
+ 'submit checkout_place_order_stripe',
29
+ this.onSubmit
30
+ );
31
+
32
+ $( document )
33
+ .on(
34
+ 'change',
35
+ '#wc-stripe-cc-form :input',
36
+ this.onCCFormChange
37
+ )
38
+ .on(
39
+ 'stripeError',
40
+ this.onError
41
+ );
42
+ },
43
+
44
+ isStripeChosen: function() {
45
+ return $( '#payment_method_stripe' ).is( ':checked' ) && ( ! $( 'input[name="wc-stripe-payment-token"]:checked' ).length || 'new' === $( 'input[name="wc-stripe-payment-token"]:checked' ).val() );
46
+ },
47
+
48
+ hasToken: function() {
49
+ return 0 < $( 'input.stripe_token' ).length;
50
+ },
51
+
52
+ block: function() {
53
+ wc_stripe_form.form.block({
54
+ message: null,
55
+ overlayCSS: {
56
+ background: '#fff',
57
+ opacity: 0.6
58
+ }
59
+ });
60
+ },
61
+
62
+ unblock: function() {
63
+ wc_stripe_form.form.unblock();
64
+ },
65
+
66
+ onError: function( e, responseObject ) {
67
+ $( '.woocommerce-error, .stripe_token' ).remove();
68
+ $( '#stripe-card-number' ).closest( 'p' ).before( '<ul class="woocommerce_error woocommerce-error"><li>' + responseObject.response.error.message + '</li></ul>' );
69
+ wc_stripe_form.unblock();
70
+ },
71
+
72
+ onSubmit: function( e ) {
73
+ if ( wc_stripe_form.isStripeChosen() && ! wc_stripe_form.hasToken() ) {
74
+ e.preventDefault();
75
+ wc_stripe_form.block();
76
+
77
+ var card = $( '#stripe-card-number' ).val(),
78
+ cvc = $( '#stripe-card-cvc' ).val(),
79
+ expires = $( '#stripe-card-expiry' ).payment( 'cardExpiryVal' ),
80
+ first_name = $( '#billing_first_name' ).length ? $( '#billing_first_name' ).val() : wc_stripe_params.billing_first_name,
81
+ last_name = $( '#billing_last_name' ).length ? $( '#billing_last_name' ).val() : wc_stripe_params.billing_last_name,
82
+ address = {
83
+
84
+ },
85
+ data = {
86
+ number : card,
87
+ cvc : cvc,
88
+ exp_month: parseInt( expires['month'] ) || 0,
89
+ exp_year : parseInt( expires['year'] ) || 0,
90
+ name : first_name + ' ' + last_name
91
+
92
+ };
93
+
94
+ if ( jQuery('#billing_address_1').length > 0 ) {
95
+ data.address_line1 = $( '#billing_address_1' ).val();
96
+ data.address_line2 = $( '#billing_address_2' ).val();
97
+ data.address_state = $( '#billing_state' ).val();
98
+ data.address_city = $( '#billing_city' ).val();
99
+ data.address_zip = $( '#billing_postcode' ).val();
100
+ data.address_country = $( '#billing_country' ).val();
101
+ } else if ( data.address_line1 ) {
102
+ data.address_line1 = wc_stripe_params.billing_address_1;
103
+ data.address_line2 = wc_stripe_params.billing_address_2;
104
+ data.address_state = wc_stripe_params.billing_state;
105
+ data.address_city = wc_stripe_params.billing_city;
106
+ data.address_zip = wc_stripe_params.billing_postcode;
107
+ data.address_country = wc_stripe_params.billing_country;
108
+ }
109
+
110
+ Stripe.createToken( data, wc_stripe_form.onStripeReponse );
111
+
112
+ // Prevent form submitting
113
+ return false;
114
+ }
115
+ },
116
+
117
+ onCCFormChange: function() {
118
+ $( '.woocommerce-error, .stripe_token' ).remove();
119
+ },
120
+
121
+ onStripeReponse: function( status, response ) {
122
+ if ( response.error ) {
123
+ $( document ).trigger( 'stripeError', { response: response } );
124
+ } else {
125
+ // token contains id, last4, and card type
126
+ var token = response['id'];
127
+
128
+ // insert the token into the form so it gets submitted to the server
129
+ wc_stripe_form.form.append( "<input type='hidden' class='stripe_token' name='stripe_token' value='" + token + "'/>" );
130
+ wc_stripe_form.form.submit();
131
+ }
132
+ }
133
+ };
134
+
135
+ wc_stripe_form.init( $( "form.checkout, form#order_review, form#add_payment_method" ) );
136
+ } );
assets/js/stripe_checkout.js ADDED
@@ -0,0 +1,135 @@
1
+ jQuery( function( $ ) {
2
+
3
+ /**
4
+ * Object to handle Stripe payment forms.
5
+ */
6
+ var wc_stripe_form = {
7
+
8
+ /**
9
+ * Initialize e handlers and UI state.
10
+ */
11
+ init: function( form ) {
12
+ this.form = form;
13
+ this.stripe_submit = false;
14
+
15
+ $( this.form )
16
+ // We need to bind directly to the click (and not checkout_place_order_stripe) to avoid popup blockers
17
+ // especially on mobile devices (like on Chrome for iOS) from blocking StripeCheckout.open from opening a tab
18
+ .on( 'click', '#place_order', this.onSubmit )
19
+
20
+ // WooCommerce lets us return a false on checkout_place_order_{gateway} to keep the form from submitting
21
+ .on( 'submit checkout_place_order_stripe' );
22
+ },
23
+
24
+ isStripeChosen: function() {
25
+ return $( '#payment_method_stripe' ).is( ':checked' ) && ( ! $( 'input[name="wc-stripe-payment-token"]:checked' ).length || 'new' === $( 'input[name="wc-stripe-payment-token"]:checked' ).val() );
26
+ },
27
+
28
+ isStripeModalNeeded: function( e ) {
29
+ var token = wc_stripe_form.form.find( 'input.stripe_token' );
30
+
31
+ // If this is a stripe submission (after modal) and token exists, allow submit.
32
+ if ( wc_stripe_form.stripe_submit && token ) {
33
+ return false;
34
+ }
35
+
36
+ // Don't affect submission if modal is not needed.
37
+ if ( ! wc_stripe_form.isStripeChosen() ) {
38
+ return false;
39
+ }
40
+
41
+ // Don't open modal if required fields are not complete
42
+ if ( $( 'input#terms' ).length === 1 && $( 'input#terms:checked' ).length === 0 ) {
43
+ return false;
44
+ }
45
+
46
+ if ( $( '#createaccount' ).is( ':checked' ) && $( '#account_password' ).length && $( '#account_password' ).val() === '' ) {
47
+ return false;
48
+ }
49
+
50
+ // check to see if we need to validate shipping address
51
+ if ( $( '#ship-to-different-address-checkbox' ).is( ':checked' ) ) {
52
+ $required_inputs = $( '.woocommerce-billing-fields .validate-required, .woocommerce-shipping-fields .validate-required' );
53
+ } else {
54
+ $required_inputs = $( '.woocommerce-billing-fields .validate-required' );
55
+ }
56
+
57
+ if ( $required_inputs.length ) {
58
+ var required_error = false;
59
+
60
+ $required_inputs.each( function() {
61
+ if ( $( this ).find( 'input.input-text, select' ).not( $( '#account_password, #account_username' ) ).val() === '' ) {
62
+ required_error = true;
63
+ }
64
+ });
65
+
66
+ if ( required_error ) {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ return true;
72
+ },
73
+
74
+ block: function() {
75
+ wc_stripe_form.form.block({
76
+ message: null,
77
+ overlayCSS: {
78
+ background: '#fff',
79
+ opacity: 0.6
80
+ }
81
+ });
82
+ },
83
+
84
+ unblock: function() {
85
+ wc_stripe_form.form.unblock();
86
+ },
87
+
88
+ onClose: function() {
89
+ wc_stripe_form.unblock();
90
+ },
91
+
92
+ onSubmit: function( e ) {
93
+ if ( wc_stripe_form.isStripeModalNeeded() ) {
94
+ e.preventDefault();
95
+
96
+ // Capture submittal and open stripecheckout
97
+ var $form = wc_stripe_form.form,
98
+ $data = $( '#stripe-payment-data' ),
99
+ token = $form.find( 'input.stripe_token' );
100
+
101
+ token.val( '' );
102
+
103
+ var token_action = function( res ) {
104
+ $form.find( 'input.stripe_token' ).remove();
105
+ $form.append( '<input type="hidden" class="stripe_token" name="stripe_token" value="' + res.id + '"/>' );
106
+ wc_stripe_form.stripe_submit = true;
107
+ $form.submit();
108
+ };
109
+
110
+ StripeCheckout.open({
111
+ key : wc_stripe_params.key,
112
+ address : false,
113
+ amount : $data.data( 'amount' ),
114
+ name : $data.data( 'name' ),
115
+ description : $data.data( 'description' ),
116
+ currency : $data.data( 'currency' ),
117
+ image : $data.data( 'image' ),
118
+ bitcoin : $data.data( 'bitcoin' ),
119
+ locale : $data.data( 'locale' ),
120
+ refund_mispayments: true, // for bitcoin payments let Stripe handle refunds if too little is paid
121
+ email : $( '#billing_email' ).val() || $data.data( 'email' ),
122
+ "panel-label" : $data.data( 'panel-label' ),
123
+ token : token_action,
124
+ closed : wc_stripe_form.onClose()
125
+ });
126
+
127
+ return false;
128
+ }
129
+
130
+ return true;
131
+ }
132
+ };
133
+
134
+ wc_stripe_form.init( $( "form.checkout, form#order_review, form#add_payment_method" ) );
135
+ } );
changelog.txt ADDED
@@ -0,0 +1,309 @@
1
+ *** Changelog ***
2
+
3
+ = 3.0.0 =
4
+ * First public WordPress.org release.
5
+ * Refactor for WC 2.6 and above. Legacy support for 2.5.
6
+ * Improved saved card handling using tokenization API in WooCommerce.
7
+
8
+ = 2.6.12 - 2016.04.13 =
9
+ * Fix - When saved cards option is enabled with no cards on file, CC field was hidden.
10
+
11
+ = 2.6.11 - 2016.04.11 =
12
+ * Add - Option to set a default card in manage card section and detect previous card paid by subscription.
13
+ * Fix - Admin notice link when key is not added and when addons are present.
14
+
15
+ = 2.6.10 - 2016.03.16 =
16
+ * Tweak - Add logging mechanism. New 'Logging' option is added in Stripe gateway setting to enable logging.
17
+ * Fix - Allow language files to be located outside of plugin directory
18
+
19
+ = 2.6.9 - 2016.02.29 =
20
+ * Tweak - Allow mechanism to override Stripe JS error handler. See https://gist.github.com/gedex/240492f479c7443e4780
21
+ for an example to override error handler with simple alert.
22
+
23
+ = 2.6.8 - 2016.02.11 =
24
+ * Tweak - Include card brand in saved cards radio label class
25
+ * Tweak - Add action when deleting card
26
+ * Tweak - Add actions for add_card and add_customer
27
+ * Tweak - Add support for automatic localisation in Stripe Checkout modal
28
+ * Fix - Check for Stripe error code emptiness before returning the WP_Error
29
+
30
+ = 2.6.7 - 2015.12.17 =
31
+ * Fix is_available SSL check to also work properly on hosts that always serve HTTPS
32
+
33
+ = 2.6.6 - 2015.12.03 =
34
+ * Fix a JavaScript bug introduced in 2.6.4 that caused checkout with a saved card to fail
35
+
36
+ = 2.6.5 - 2015.12.02 =
37
+ * Do not require a card id when updating a subscription payment method
38
+
39
+ = 2.6.4 - 2015.11.20 =
40
+ * Fix a JavaScript bug that caused the Stripe Checkout popup to be blocked on Chrome for iOS
41
+
42
+ = 2.6.3 - 2015.11.12 =
43
+ * Add metadata to subscription payments in stripe dashboard to indicate whether it is the initial or a recurring payment
44
+
45
+ = 2.6.2 - 2015.11.06 =
46
+ * Fix bug that would cause multiple subscriptions to not be supported under certain circumstances
47
+
48
+ = 2.6.1 - 2015.09.15 =
49
+ * Unset source if not set during pre-order release payments.
50
+ * Store customer ID if not logged in for pre-order payments.
51
+
52
+ = 2.6.0 - 2015.09.02 =
53
+ * Subscriptions 2.0 support.
54
+
55
+ = 2.5.4 - 2015.08.11 =
56
+ * Tweak - Terms and conditions error styling when required
57
+ * Tweak - Account password error styling when required
58
+
59
+ = 2.5.3 - 2015.7.28 =
60
+ * Added - Filter to prevent Stripe from sending its own receipts "wc_stripe_send_stripe_receipt"
61
+
62
+ = 2.5.2 - 2015.07.19 =
63
+ * Fix - Removed deprecated add_error function
64
+ * Tweak - Improve error message when Stripe checkout function is used
65
+
66
+ = 2.5.1 - 2015.07.01 =
67
+ * Fix - Only send receipt_email when set.
68
+
69
+ = 2.5.0 - 2015.05.11 =
70
+ * Update to API version 2015-04-07
71
+ * Feature - Support authorize on subscriptions first payment.
72
+ * Tweak - Option labels.
73
+ * Tweak - Safe remote GET.
74
+ * Tweak - SSLVerify true.
75
+ * Tweak - Update card icons.
76
+ * Tweak - Pass receipt email.
77
+
78
+ = 2.4.3 - 2015.05.11 =
79
+ * Fix - fixed validation issue when account creation is not checked
80
+ * Update - Stripe checkout JS API v2
81
+
82
+ = 2.4.2 - 2015.03.23 =
83
+ * Fix - Create account password field was not being validated
84
+
85
+ = 2.4.1 - 2015.03.20 =
86
+ * Fix - Undefined JS error due to deprecated ajax_loader_url
87
+ * Fix - When using Stripe checkout JS, some form required fields were not validating
88
+
89
+ = 2.4.0 - 2015.02.20 =
90
+ * Added support for bitcoin currency
91
+
92
+ = 2.3.0 - 2015.01.31 =
93
+ * Added 'wc_stripe_description' filter to allow filtering of payment description.
94
+ * Added order_review handling for stripe checkout.
95
+ * Mark order as failed is Stripe API call fails
96
+ * Allow valid HTML in Stripe Description
97
+ * Fix settings link
98
+ * use get_order_currency() when generating payment args, rather than always using store currency.
99
+ * Fix fees where not logged correctly when using authorized first capture later
100
+ * Retry payment if customer_id is invalid.
101
+
102
+ = 2.2.8 - 2014.11.21 =
103
+ * Save card/customer id for regular orders.
104
+
105
+ = 2.2.7 - 2014.11.20 =
106
+ * Fixed all instances where order IDs were used instead of user IDs.
107
+ * Update orignal order card/customer ids for renewals.
108
+ * Add reasons to refunds.
109
+
110
+ = 2.2.6 - 2014.11.18 =
111
+ * Stripe card ID should be taken from the order, not the user.
112
+ * Fix order_meta_query.
113
+
114
+ = 2.2.5 - 2014.11.06 =
115
+ * Round totals to 2 decimals so when we multiply by 100 we're sure we've got an integer.
116
+
117
+ = 2.2.4 - 2014.10.01 =
118
+ * Fix card display for subscriptions.
119
+
120
+ = 2.2.3 - 2014.10.01 =
121
+ * Fixed textdomain name
122
+
123
+ = 2.2.2 - 2014.09.23 =
124
+ * Set API version to 2014-09-08.
125
+ * Fixed card display (type->brand).
126
+
127
+ = 2.2.1 - 2014.09.15 =
128
+ * Fix strict standards warning.
129
+
130
+ = 2.2.0 - 2014.09.01 =
131
+ * Replaced woocommerce_get_template (deprecated) with wc_get_template.
132
+ * Tweak refund support.
133
+ * Support for pre-orders.
134
+ * Fixed typo.
135
+
136
+ = 2.1.0 - 2014.08.06 =
137
+ * Associate stripe customers with wp users.
138
+ * Refactored saved card code.
139
+ * Use Stripe API to get and delete saved cards.
140
+ * Updated subscriptions integration for saved cards.
141
+ * WC 2.2 - Store transaction ID.
142
+ * WC 2.2 - Refund support.
143
+
144
+ = 2.0.4 - 2014.07.31 =
145
+ * Tweaked the stripe checkout submission method.
146
+
147
+ = 2.0.3 - 2014.07.25 =
148
+ * wc_stripe_manage_saved_cards_url filter.
149
+ * Zero decimal currency handling.
150
+ * Only open stripe model when required fields are completed.
151
+
152
+ = 2.0.2 - 2014.06.06 =
153
+ * Fix use of saved cards on subscriptions.
154
+
155
+ = 2.0.1 - 2014.05.29 =
156
+ * Fix ajax loading gif.
157
+ * Fix notices.
158
+ * Fix stray comma in stripe.js.
159
+ * Prompt user to accept terms before showing stripe checkout modal.
160
+
161
+ = 2.0.0 - 2014.05.21 =
162
+ * Added the WC credit_card_form - this extension now requires WC 2.1+
163
+ * Option to disable saved cards
164
+ * Refactored code base
165
+ * Fix jquery notices
166
+ * Fix settings page links
167
+ * woocommerce_stripe_request_body filter
168
+ * Store fees for subscriptions
169
+
170
+ = 1.8.6 - 2014.05.20 =
171
+ * correct SSl message
172
+ * decode get_bloginfo( 'name' ) for plain text display
173
+
174
+ = 1.8.5 - 2014.05.10 =
175
+ * Updated textdomains
176
+ * date_i18n
177
+ * Improve stripe checkout flow - pop up on the checkout button click
178
+
179
+ = 1.8.4 - 2014.04.01 =
180
+ * Fix updating credit card used for future subscription payments when paying for a failed subscription renewal order with a new credit card.
181
+
182
+ = 1.8.3 - 2014.02.13 =
183
+ * Fix fatal error for subscription payments of deleted products.
184
+
185
+ = 1.8.2 - 2014.02.06 =
186
+ * Fix notice on card delete
187
+
188
+ = 1.8.1 - 2014.01.28 =
189
+ * set default for $checked
190
+
191
+ = 1.8.0 - 2014.01.08 =
192
+ * Checked compatibility with 2013-12-03 API
193
+ * 2.1 compatibility
194
+ * Pre-filled email address when using stripe checkout
195
+
196
+ = 1.7.6 - 2013.12.02 =
197
+ * Fix card display
198
+
199
+ = 1.7.5 - 2013.11.27 =
200
+ * Show payment method for subscriptions on account page
201
+
202
+ = 1.7.4 - 2013.11.20 =
203
+ * Expand/close when using saved cards.
204
+ * Use balance_transaction to get and store fees
205
+
206
+ = 1.7.3 - 2013.11.01 =
207
+ * Default to saved card
208
+
209
+ = 1.7.2 - 2013.11.01 =
210
+ * Added missing global in update_failing_payment_method
211
+
212
+ = 1.7.1 - 2013.09.28 =
213
+ * Remove non-existant (yet) function
214
+
215
+ = 1.7.0 - 2013.09.25 =
216
+ * Different credit card image for US than for other countries + a filter.
217
+ * Support for upcoming version of subscriptions.
218
+ * Add new woocommerce_stripe_month_display filter
219
+
220
+ = 1.6.0 - 2013.09.02 =
221
+ * Option to define a Stripe Checkout Image
222
+ * Removed currency check due to beta rollout
223
+
224
+ = 1.5.14 - 2013.08.12 =
225
+ * New cards format for subscriptions class.
226
+
227
+ = 1.5.13 - 2013.07.24 =
228
+ * Updated customer response object handler to work with new cards format.
229
+ * Fixed delete card button
230
+
231
+ = 1.5.12 - 2013.07.24 =
232
+ * EUR support for Stripe Beta
233
+
234
+ = 1.5.11 - 2013.07.17 =
235
+ * Workaround for stripe error messages.
236
+
237
+ = 1.5.10 - 2013.06.28 =
238
+ * Store charge ID, fee in meta
239
+
240
+ = 1.5.9 - 2013.06.28 =
241
+ * Capture true default
242
+
243
+ = 1.5.8 - 2013.06.18 =
244
+ * Add currency to stripe checkout js
245
+ * Authorize-only mode. Captures payment when order is made processing.
246
+
247
+ = 1.5.7 - 2013.06.15 =
248
+ * Added 'capture' option should you wish to authorize only. Authorized orders are on-hold. Processed orders capture the charge automatically.
249
+
250
+ = 1.5.6 - 2013.06.03 =
251
+ * added data-currency to stripe-checkout
252
+
253
+ = 1.5.5 - 2013.04.26 =
254
+ * Allow card re-entry in stripe checkout after errors.
255
+
256
+ = 1.5.4 - 2013.04.19 =
257
+ * GBP fix
258
+
259
+ = 1.5.3 - 2013.04.15 =
260
+ * Support GBP currency code (For UK Beta)
261
+
262
+ = 1.5.2 - 2013.04.09 =
263
+ * Send billing city to stripe
264
+
265
+ = 1.5.1 - 2013.01.24 =
266
+ * Add support for changing a subscription's recurring amount
267
+
268
+ = 1.5.0 - 2013.01.18 =
269
+ * Supports Stripe Checkout https://stripe.com/docs/checkout
270
+
271
+ = 1.4.0 - 2013.01.18 =
272
+ * WC 2.0 Compat
273
+
274
+ = 1.3.5 - 2012.12.05 =
275
+ * Pass address fields to stripe.js on pay page.
276
+
277
+ = 1.3.4 - 2012.12.05 =
278
+ * Updater
279
+
280
+ = 1.3.3 - 2012.10.22 =
281
+ * Fix CAD check
282
+
283
+ = 1.3.2 - 2012.10.15 =
284
+ * Fixed bug causing settings to not show when using CAD
285
+
286
+ = 1.3.1 - 2012.10.11 =
287
+ * Add support for changing subscription next payment date
288
+ * Remove order meta from subscription renewal orders
289
+
290
+ = 1.3 - 2012.09.20 =
291
+ * Allowed canadian dollars - Stripe is beta testing support for Canada
292
+
293
+ = 1.2.1 - 2012.09.11 =
294
+ * Fix text mode SSL logic
295
+
296
+ = 1.2 - 2012.09.01 =
297
+ * SSL not required in TEST MODE
298
+ * Saved cards - store customer tokens and let users pay again using the same card
299
+ * Subscriptions use a single customer, rather than per-order
300
+ * Only load JS on checkout
301
+
302
+ = 1.1 - 2012.06.19 =
303
+ * Update woo updater
304
+ * Class name update
305
+ * Stripe JS for added security - you will need to re-enter keys and ensure you are using WooCommerce 1.5.8
306
+ * Subscriptions support (requires WC Subscriptions addon)
307
+
308
+ = 1.0 - 2011.12.08 =
309
+ * First Release
includes/class-wc-gateway-stripe-addons.php ADDED
@@ -0,0 +1,411 @@
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit;
4
+ }
5
+
6
+ /**
7
+ * WC_Gateway_Stripe_Addons class.
8
+ *
9
+ * @extends WC_Gateway_Stripe
10
+ */
11
+ class WC_Gateway_Stripe_Addons extends WC_Gateway_Stripe {
12
+
13
+ /**
14
+ * Constructor
15
+ */
16
+ public function __construct() {
17
+ parent::__construct();
18
+
19
+ if ( class_exists( 'WC_Subscriptions_Order' ) ) {
20
+ add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, array( $this, 'scheduled_subscription_payment' ), 10, 2 );
21
+ add_action( 'wcs_resubscribe_order_created', array( $this, 'delete_resubscribe_meta' ), 10 );
22
+ add_action( 'wcs_renewal_order_created', array( $this, 'delete_renewal_meta' ), 10 );
23
+ add_action( 'woocommerce_subscription_failing_payment_method_updated_stripe', array( $this, 'update_failing_payment_method' ), 10, 2 );
24
+
25
+ // display the credit card used for a subscription in the "My Subscriptions" table
26
+ add_filter( 'woocommerce_my_subscriptions_payment_method', array( $this, 'maybe_render_subscription_payment_method' ), 10, 2 );
27
+
28
+ // allow store managers to manually set Stripe as the payment method on a subscription
29
+ add_filter( 'woocommerce_subscription_payment_meta', array( $this, 'add_subscription_payment_meta' ), 10, 2 );
30
+ add_filter( 'woocommerce_subscription_validate_payment_meta', array( $this, 'validate_subscription_payment_meta' ), 10, 2 );
31
+ }
32
+
33
+ if ( class_exists( 'WC_Pre_Orders_Order' ) ) {
34
+ add_action( 'wc_pre_orders_process_pre_order_completion_payment_' . $this->id, array( $this, 'process_pre_order_release_payment' ) );
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Is $order_id a subscription?
40
+ * @param int $order_id
41
+ * @return boolean
42
+ */
43
+ protected function is_subscription( $order_id ) {
44
+ return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) );
45
+ }
46
+
47
+ /**
48
+ * Is $order_id a pre-order?
49
+ * @param int $order_id
50
+ * @return boolean
51
+ */
52
+ protected function is_pre_order( $order_id ) {
53
+ return ( class_exists( 'WC_Pre_Orders_Order' ) && WC_Pre_Orders_Order::order_contains_pre_order( $order_id ) );
54
+ }
55
+
56
+ /**
57
+ * Process the payment based on type.
58
+ * @param int $order_id
59
+ * @return array
60
+ */
61
+ public function process_payment( $order_id, $retry = true, $force_customer = false ) {
62
+ if ( $this->is_subscription( $order_id ) ) {
63
+ // Regular payment with force customer enabled
64
+ return parent::process_payment( $order_id, true, true );
65
+
66
+ } elseif ( $this->is_pre_order( $order_id ) ) {
67
+ return $this->process_pre_order( $order_id, $retry, $force_customer );
68
+
69
+ } else {
70
+ return parent::process_payment( $order_id, $retry, $force_customer );
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Updates other subscription sources.
76
+ */
77
+ protected function save_source( $order, $source ) {
78
+ parent::save_source( $order, $source );
79
+
80
+ // Also store it on the subscriptions being purchased or paid for in the order
81
+ if ( wcs_order_contains_subscription( $order->id ) ) {
82
+ $subscriptions = wcs_get_subscriptions_for_order( $order->id );
83
+ } elseif ( wcs_order_contains_renewal( $order->id ) ) {
84
+ $subscriptions = wcs_get_subscriptions_for_renewal_order( $order->id );
85
+ } else {
86
+ $subscriptions = array();
87
+ }
88
+
89
+ foreach( $subscriptions as $subscription ) {
90
+ update_post_meta( $subscription->id, '_stripe_customer_id', $source->customer );
91
+ update_post_meta( $subscription->id, '_stripe_card_id', $source->source );
92
+ }
93
+ }
94
+
95
+ /**
96
+ * process_subscription_payment function.
97
+ * @param mixed $order
98
+ * @param int $amount (default: 0)
99
+ * @param string $stripe_token (default: '')
100
+ * @param bool initial_payment
101
+ */
102
+ public function process_subscription_payment( $order = '', $amount = 0 ) {
103
+ if ( $amount * 100 < 50 ) {
104
+ return new WP_Error( 'stripe_error', __( 'Sorry, the minimum allowed order total is 0.50 to use this payment method.', 'woocommerce-gateway-stripe' ) );
105
+ }
106
+
107
+ // Get source from order
108
+ $source = $this->get_order_source( $order );
109
+
110
+ // If no order source was defined, use user source instead.
111
+ if ( ! $source->customer ) {
112
+ $source = $this->get_source( $order->customer_user );
113
+ }
114
+
115
+ // Or fail :(
116
+ if ( ! $source->customer ) {
117
+ return new WP_Error( 'stripe_error', __( 'Customer not found', 'woocommerce-gateway-stripe' ) );
118
+ }
119
+
120
+ WC_Stripe::log( "Info: Begin processing subscriotion payment for order {$order->id} for the amount of {$amount}" );
121
+
122
+ // Make the request
123
+ $request = $this->generate_payment_request( $order, $source );
124
+ $request['capture'] = 'true';
125
+ $request['amount'] = $this->get_stripe_amount( $amount, $request['currency'] );
126
+ $request['metadata'] = array(
127
+ 'payment_type' => 'recurring'
128
+ );
129
+ $response = WC_Stripe_API::request( $request );
130
+
131
+ // Process valid response
132
+ if ( ! is_wp_error( $response ) ) {
133
+ $this->process_response( $response, $order );
134
+ }
135
+
136
+ return $response;
137
+ }
138
+
139
+ /**
140
+ * Process the pre-order
141
+ * @param int $order_id
142
+ * @return array
143
+ */
144
+ public function process_pre_order( $order_id, $retry, $force_customer ) {
145
+ if ( WC_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ) ) {
146
+ try {
147
+ $order = wc_get_order( $order_id );
148
+
149
+ if ( $order->get_total() * 100 < 50 ) {
150
+ throw new Exception( __( 'Sorry, the minimum allowed order total is 0.50 to use this payment method.', 'woocommerce-gateway-stripe' ) );
151
+ }
152
+
153
+ $source = $this->get_source( get_current_user_id(), true );
154
+
155
+ // We need a source on file to continue.
156
+ if ( empty( $source->customer ) || empty( $source->source ) ) {
157
+ throw new Exception( __( 'Unable to store payment details. Please try again.', 'woocommerce-gateway-stripe' ) );
158
+ }
159
+
160
+ // Store source to order meta
161
+ $this->save_source( $order, $source );
162
+
163
+ // Reduce stock levels
164
+ $order->reduce_order_stock();
165
+
166
+ // Remove cart
167
+ WC()->cart->empty_cart();
168
+
169
+ // Is pre ordered!
170
+ WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
171
+
172
+ // Return thank you page redirect
173
+ return array(
174
+ 'result' => 'success',
175
+ 'redirect' => $this->get_return_url( $order )
176
+ );
177
+ } catch ( Exception $e ) {
178
+ wc_add_notice( $e->getMessage(), 'error' );
179
+ return;
180
+ }
181
+ } else {
182
+ return parent::process_payment( $order_id, $retry, $force_customer );
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Process a pre-order payment when the pre-order is released
188
+ * @param WC_Order $order
189
+ * @return void
190
+ */
191
+ public function process_pre_order_release_payment( $order ) {
192
+ try {
193
+ // Define some callbacks if the first attempt fails.
194
+ $retry_callbacks = array(
195
+ 'remove_order_source_before_retry',
196
+ 'remove_order_customer_before_retry',
197
+ );
198
+
199
+ while ( 1 ) {
200
+ $source = $this->get_order_source( $order );
201
+ $response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source ) );
202
+
203
+ if ( is_wp_error( $response ) ) {
204
+ if ( 0 === sizeof( $retry_callbacks ) ) {
205
+ throw new Exception( $response->get_error_message() );
206
+ } else {
207
+ $retry_callback = array_shift( $retry_callbacks );
208
+ call_user_func( array( $this, $retry_callback ), $order );
209
+ }
210
+ } else {
211
+ // Successful
212
+ $this->process_response( $response, $order );
213
+ break;
214
+ }
215
+ }
216
+
217
+ } catch ( Exception $e ) {
218
+ $order_note = sprintf( __( 'Stripe Transaction Failed (%s)', 'woocommerce-gateway-stripe' ), $e->getMessage() );
219
+
220
+ // Mark order as failed if not already set,
221
+ // otherwise, make sure we add the order note so we can detect when someone fails to check out multiple times
222
+ if ( ! $order->has_status( 'failed' ) ) {
223
+ $order->update_status( 'failed', $order_note );
224
+ } else {
225
+ $order->add_order_note( $order_note );
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Don't transfer Stripe customer/token meta to resubscribe orders.
232
+ * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
233
+ */
234
+ public function delete_resubscribe_meta( $resubscribe_order ) {
235
+ delete_post_meta( $resubscribe_order->id, '_stripe_customer_id' );
236
+ delete_post_meta( $resubscribe_order->id, '_stripe_card_id' );
237
+ $this->delete_renewal_meta( $resubscribe_order );
238
+ }
239
+
240
+ /**
241
+ * Don't transfer Stripe fee/ID meta to renewal orders.
242
+ * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
243
+ */
244
+ public function delete_renewal_meta( $renewal_order ) {
245
+ delete_post_meta( $renewal_order->id, 'Stripe Fee' );
246
+ delete_post_meta( $renewal_order->id, 'Net Revenue From Stripe' );
247
+ delete_post_meta( $renewal_order->id, 'Stripe Payment ID' );
248
+ return $renewal_order;
249
+ }
250
+
251
+ /**
252
+ * scheduled_subscription_payment function.
253
+ *
254
+ * @param $amount_to_charge float The amount to charge.
255
+ * @param $renewal_order WC_Order A WC_Order object created to record the renewal payment.
256
+ */
257
+ public function scheduled_subscription_payment( $amount_to_charge, $renewal_order ) {
258
+ // Define some callbacks if the first attempt fails.
259
+ $retry_callbacks = array(
260
+ 'remove_order_source_before_retry',
261
+ 'remove_order_customer_before_retry',
262
+ );
263
+
264
+ while ( 1 ) {
265
+ $response = $this->process_subscription_payment( $renewal_order, $amount_to_charge );
266
+
267
+ if ( is_wp_error( $response ) ) {
268
+ if ( 0 === sizeof( $retry_callbacks ) ) {
269
+ $renewal_order->update_status( 'failed', sprintf( __( 'Stripe Transaction Failed (%s)', 'woocommerce-gateway-stripe' ), $response->get_error_message() ) );
270
+ break;
271
+ } else {
272
+ $retry_callback = array_shift( $retry_callbacks );
273
+ call_user_func( array( $this, $retry_callback ), $renewal_order );
274
+ }
275
+ } else {
276
+ // Successful
277
+ break;
278
+ }
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Remove order meta
284
+ * @param object $order
285
+ */
286
+ public function remove_order_source_before_retry( $order ) {
287
+ delete_post_meta( $order->id, '_stripe_card_id' );
288
+ }
289
+
290
+ /**
291
+ * Remove order meta
292
+ * @param object $order
293
+ */
294
+ public function remove_order_customer_before_retry( $order ) {
295
+ delete_post_meta( $order->id, '_stripe_customer_id' );
296
+ }
297
+
298
+ /**
299
+ * Update the customer_id for a subscription after using Stripe to complete a payment to make up for
300
+ * an automatic renewal payment which previously failed.
301
+ *
302
+ * @access public
303
+ * @param WC_Subscription $subscription The subscription for which the failing payment method relates.
304
+ * @param WC_Order $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment).
305
+ * @return void
306
+ */
307
+ public function update_failing_payment_method( $subscription, $renewal_order ) {
308
+ update_post_meta( $subscription->id, '_stripe_customer_id', $renewal_order->stripe_customer_id );
309
+ update_post_meta( $subscription->id, '_stripe_card_id', $renewal_order->stripe_card_id );
310
+ }
311
+
312
+ /**
313
+ * Include the payment meta data required to process automatic recurring payments so that store managers can
314
+ * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
315
+ *
316
+ * @since 2.5
317
+ * @param array $payment_meta associative array of meta data required for automatic payments
318
+ * @param WC_Subscription $subscription An instance of a subscription object
319
+ * @return array
320
+ */
321
+ public function add_subscription_payment_meta( $payment_meta, $subscription ) {
322
+ $payment_meta[ $this->id ] = array(
323
+ 'post_meta' => array(
324
+ '_stripe_customer_id' => array(
325
+ 'value' => get_post_meta( $subscription->id, '_stripe_customer_id', true ),
326
+ 'label' => 'Stripe Customer ID',
327
+ ),
328
+ '_stripe_card_id' => array(
329
+ 'value' => get_post_meta( $subscription->id, '_stripe_card_id', true ),
330
+ 'label' => 'Stripe Card ID',
331
+ ),
332
+ ),
333
+ );
334
+ return $payment_meta;
335
+ }
336
+
337
+ /**
338
+ * Validate the payment meta data required to process automatic recurring payments so that store managers can
339
+ * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
340
+ *
341
+ * @since 2.5
342
+ * @param string $payment_method_id The ID of the payment method to validate
343
+ * @param array $payment_meta associative array of meta data required for automatic payments
344
+ * @return array
345
+ */
346
+ public function validate_subscription_payment_meta( $payment_method_id, $payment_meta ) {
347
+ if ( $this->id === $payment_method_id ) {
348
+
349
+ if ( ! isset( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) || empty( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) ) {
350
+ throw new Exception( 'A "_stripe_customer_id" value is required.' );
351
+ } elseif ( 0 !== strpos( $payment_meta['post_meta']['_stripe_customer_id']['value'], 'cus_' ) ) {
352
+ throw new Exception( 'Invalid customer ID. A valid "_stripe_customer_id" must begin with "cus_".' );
353
+ }
354
+
355
+ if ( ! empty( $payment_meta['post_meta']['_stripe_card_id']['value'] ) && 0 !== strpos( $payment_meta['post_meta']['_stripe_card_id']['value'], 'card_' ) ) {
356
+ throw new Exception( 'Invalid card ID. A valid "_stripe_card_id" must begin with "card_".' );
357
+ }
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Render the payment method used for a subscription in the "My Subscriptions" table
363
+ *
364
+ * @since 1.7.5
365
+ * @param string $payment_method_to_display the default payment method text to display
366
+ * @param WC_Subscription $subscription the subscription details
367
+ * @return string the subscription payment method
368
+ */
369
+ public function maybe_render_subscription_payment_method( $payment_method_to_display, $subscription ) {
370
+ // bail for other payment methods
371
+ if ( $this->id !== $subscription->payment_method || ! $subscription->customer_user ) {
372
+ return $payment_method_to_display;
373
+ }
374
+
375
+ $stripe_customer = new WC_Stripe_Customer();
376
+ $stripe_customer_id = get_post_meta( $subscription->id, '_stripe_customer_id', true );
377
+ $stripe_card_id = get_post_meta( $subscription->id, '_stripe_card_id', true );
378
+
379
+ // If we couldn't find a Stripe customer linked to the subscription, fallback to the user meta data.
380
+ if ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) {
381
+ $user_id = $subscription->customer_user;
382
+ $stripe_customer_id = get_user_meta( $user_id, '_stripe_customer_id', true );
383
+ $stripe_card_id = get_user_meta( $user_id, '_stripe_card_id', true );
384
+ }
385
+
386
+ // If we couldn't find a Stripe customer linked to the account, fallback to the order meta data.
387
+ if ( ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) && false !== $subscription->order ) {
388
+ $stripe_customer_id = get_post_meta( $subscription->order->id, '_stripe_customer_id', true );
389
+ $stripe_card_id = get_post_meta( $subscription->order->id, '_stripe_card_id', true );
390
+ }
391
+
392
+ $stripe_customer->set_id( $stripe_customer_id );
393
+ $cards = $stripe_customer->get_cards();
394
+
395
+ if ( $cards ) {
396
+ $found_card = false;
397
+ foreach ( $cards as $card ) {
398
+ if ( $card->id === $stripe_card_id ) {
399
+ $found_card = true;
400
+ $payment_method_to_display = sprintf( __( 'Via %s card ending in %s', 'woocommerce-gateway-stripe' ), ( isset( $card->type ) ? $card->type : $card->brand ), $card->last4 );
401
+ break;
402
+ }
403
+ }
404
+ if ( ! $found_card ) {
405
+ $payment_method_to_display = sprintf( __( 'Via %s card ending in %s', 'woocommerce-gateway-stripe' ), ( isset( $cards[0]->type ) ? $cards[0]->type : $cards[0]->brand ), $cards[0]->last4 );
406
+ }
407
+ }
408
+
409
+ return $payment_method_to_display;
410
+ }
411
+ }
includes/class-wc-gateway-stripe.php ADDED
@@ -0,0 +1,570 @@
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit;
4
+ }
5
+
6
+ /**
7
+ * WC_Gateway_Stripe class.
8
+ *
9
+ * @extends WC_Payment_Gateway
10
+ */
11
+ class WC_Gateway_Stripe extends WC_Payment_Gateway_CC {
12
+
13
+ /**
14
+ * Constructor
15
+ */
16
+ public function __construct() {
17
+ $this->id = 'stripe';
18
+ $this->method_title = __( 'Stripe', 'woocommerce-gateway-stripe' );
19
+ $this->method_description = __( 'Stripe works by adding credit card fields on the checkout and then sending the details to Stripe for verification.', 'woocommerce-gateway-stripe' );
20
+ $this->has_fields = true;
21
+ $this->view_transaction_url = 'https://dashboard.stripe.com/payments/%s';
22
+ $this->supports = array(
23
+ 'subscriptions',
24
+ 'products',
25
+ 'refunds',
26
+ 'subscription_cancellation',
27
+ 'subscription_reactivation',
28
+ 'subscription_suspension',
29
+ 'subscription_amount_changes',
30
+ 'subscription_payment_method_change', // Subs 1.n compatibility
31
+ 'subscription_payment_method_change_customer',
32
+ 'subscription_payment_method_change_admin',
33
+ 'subscription_date_changes',
34
+ 'multiple_subscriptions',
35
+ 'pre-orders',
36
+ 'tokenization',
37
+ );
38
+
39
+ // Load the form fields
40
+ $this->init_form_fields();
41
+
42
+ // Load the settings.
43
+ $this->init_settings();
44
+
45
+ // Get setting values.
46
+ $this->title = $this->get_option( 'title' );
47
+ $this->description = $this->get_option( 'description' );
48
+ $this->enabled = $this->get_option( 'enabled' );
49
+ $this->testmode = 'yes' === $this->get_option( 'testmode' );
50
+ $this->capture = 'yes' === $this->get_option( 'capture', 'yes' );
51
+ $this->stripe_checkout = 'yes' === $this->get_option( 'stripe_checkout' );
52
+ $this->stripe_checkout_locale = $this->get_option( 'stripe_checkout_locale' );
53
+ $this->stripe_checkout_image = $this->get_option( 'stripe_checkout_image', '' );
54
+ $this->saved_cards = 'yes' === $this->get_option( 'saved_cards' );
55
+ $this->secret_key = $this->testmode ? $this->get_option( 'test_secret_key' ) : $this->get_option( 'secret_key' );
56
+ $this->publishable_key = $this->testmode ? $this->get_option( 'test_publishable_key' ) : $this->get_option( 'publishable_key' );
57
+ $this->bitcoin = 'USD' === strtoupper( get_woocommerce_currency() ) && 'yes' === $this->get_option( 'stripe_bitcoin' );
58
+ $this->logging = 'yes' === $this->get_option( 'logging' );
59
+
60
+ if ( $this->stripe_checkout ) {
61
+ $this->order_button_text = __( 'Continue to payment', 'woocommerce-gateway-stripe' );
62
+ }
63
+
64
+ if ( $this->testmode ) {
65
+ $this->description .= ' ' . sprintf( __( 'TEST MODE ENABLED. In test mode, you can use the card number 4242424242424242 with any CVC and a valid expiration date or check the documentation "<a href="%s">Testing Stripe</a>" for more card numbers.', 'woocommerce-gateway-stripe' ), 'https://stripe.com/docs/testing' );
66
+ $this->description = trim( $this->description );
67
+ }
68
+
69
+ WC_Stripe_API::set_secret_key( $this->secret_key );
70
+
71
+ // Hooks
72
+ add_action( 'wp_enqueue_scripts', array( $this, 'payment_scripts' ) );
73
+ add_action( 'admin_notices', array( $this, 'admin_notices' ) );
74
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
75
+ }
76
+
77
+ /**
78
+ * get_icon function.
79
+ *
80
+ * @access public
81
+ * @return string
82
+ */
83
+ public function get_icon() {
84
+ $ext = version_compare( WC()->version, '2.6', '>=' ) ? '.svg' : '.png';
85
+ $style = version_compare( WC()->version, '2.6', '>=' ) ? 'style="margin-left: 0.3em"' : '';
86
+
87
+ $icon = '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/visa' . $ext ) . '" alt="Visa" width="32" ' . $style . ' />';
88
+ $icon .= '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/mastercard' . $ext ) . '" alt="Mastercard" width="32" ' . $style . ' />';
89
+ $icon .= '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/amex' . $ext ) . '" alt="Amex" width="32" ' . $style . ' />';
90
+
91
+ if ( 'USD' === get_woocommerce_currency() ) {
92
+ $icon .= '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/discover' . $ext ) . '" alt="Discover" width="32" ' . $style . ' />';
93
+ $icon .= '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/jcb' . $ext ) . '" alt="JCB" width="32" ' . $style . ' />';
94
+ $icon .= '<img src="' . WC_HTTPS::force_https_url( WC()->plugin_url() . '/assets/images/icons/credit-cards/diners' . $ext ) . '" alt="Diners" width="32" ' . $style . ' />';
95
+ }
96
+
97
+ if ( $this->bitcoin ) {
98
+ $icon .= '<img src="' . WC_HTTPS::force_https_url( plugins_url( '/assets/images/bitcoin' . $ext, WC_STRIPE_MAIN_FILE ) ) . '" alt="Bitcoin" width="32" ' . $style . ' />';
99
+ }
100
+
101
+ return apply_filters( 'woocommerce_gateway_icon', $icon, $this->id );
102
+ }
103
+
104
+ /**
105
+ * Get Stripe amount to pay
106
+ * @return float
107
+ */
108
+ public function get_stripe_amount( $total, $currency = '' ) {
109
+ if ( ! $currency ) {
110
+ $currency = get_woocommerce_currency();
111
+ }
112
+ switch ( strtoupper( $currency ) ) {
113
+ // Zero decimal currencies
114
+ case 'BIF' :
115
+ case 'CLP' :
116
+ case 'DJF' :
117
+ case 'GNF' :
118
+ case 'JPY' :
119
+ case 'KMF' :
120
+ case 'KRW' :
121
+ case 'MGA' :
122
+ case 'PYG' :
123
+ case 'RWF' :
124
+ case 'VND' :
125
+ case 'VUV' :
126
+ case 'XAF' :
127
+ case 'XOF' :
128
+ case 'XPF' :
129
+ $total = absint( $total );
130
+ break;
131
+ default :
132
+ $total = round( $total, 2 ) * 100; // In cents
133
+ break;
134
+ }
135
+ return $total;
136
+ }
137
+
138
+ /**
139
+ * Check if SSL is enabled and notify the user
140
+ */
141
+ public function admin_notices() {
142
+ if ( 'no' === $this->enabled ) {
143
+ return;
144
+ }
145
+
146
+ // Show message if enabled and FORCE SSL is disabled and WordpressHTTPS plugin is not detected
147
+ if ( ( function_exists( 'wc_site_is_https' ) && ! wc_site_is_https() ) && ( 'no' === get_option( 'woocommerce_force_ssl_checkout' ) && ! class_exists( 'WordPressHTTPS' ) ) ) {
148
+ echo '<div class="error"><p>' . sprintf( __( 'Stripe is enabled, but the <a href="%s">force SSL option</a> is disabled; your checkout may not be secure! Please enable SSL and ensure your server has a valid SSL certificate - Stripe will only work in test mode.', 'woocommerce-gateway-stripe' ), admin_url( 'admin.php?page=wc-settings&tab=checkout' ) ) . '</p></div>';
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Check if this gateway is enabled
154
+ */
155
+ public function is_available() {
156
+ if ( 'yes' === $this->enabled ) {
157
+ if ( ! $this->testmode && is_checkout() && ! is_ssl() ) {
158
+ return false;
159
+ }
160
+ if ( ! $this->secret_key || ! $this->publishable_key ) {
161
+ return false;
162
+ }
163
+ return true;
164
+ }
165
+ return false;
166
+ }
167
+
168
+ /**
169
+ * Initialise Gateway Settings Form Fields
170
+ */
171
+ public function init_form_fields() {
172
+ $this->form_fields = include( 'settings-stripe.php' );
173
+
174
+ wc_enqueue_js( "
175
+ jQuery( function( $ ) {
176
+ $( '#woocommerce_stripe_stripe_checkout' ).change(function(){
177
+ if ( $( this ).is( ':checked' ) ) {
178
+ $( '#woocommerce_stripe_stripe_checkout_locale, #woocommerce_stripe_stripe_bitcoin, #woocommerce_stripe_stripe_checkout_image' ).closest( 'tr' ).show();
179
+ } else {
180
+ $( '#woocommerce_stripe_stripe_checkout_locale, #woocommerce_stripe_stripe_bitcoin, #woocommerce_stripe_stripe_checkout_image' ).closest( 'tr' ).hide();
181
+ }
182
+ }).change();
183
+ });
184
+ " );
185
+ }
186
+
187
+ /**
188
+ * Payment form on checkout page
189
+ */
190
+ public function payment_fields() {
191
+ $user = wp_get_current_user();
192
+ $display_tokenization = $this->supports( 'tokenization' ) && is_checkout() && $this->saved_cards && $user->ID;
193
+
194
+ if ( $user->ID ) {
195
+ $user_email = get_user_meta( $user->ID, 'billing_email', true );
196
+ $user_email = $user_email ? $user_email : $user->user_email;
197
+ } else {
198
+ $user_email = '';
199
+ }
200
+
201
+ if ( is_add_payment_method_page() ) {
202
+ $pay_button_text = __( 'Add Card', 'woocommerce-gateway-stripe' );
203
+ } else {
204
+ $pay_button_text = '';
205
+ }
206
+
207
+ echo '<div
208
+ id="stripe-payment-data"
209
+ data-panel-label="' . esc_attr( $pay_button_text ) . '"
210
+ data-description=""
211
+ data-email="' . esc_attr( $user_email ) . '"
212
+ data-amount="' . esc_attr( $this->get_stripe_amount( WC()->cart->total ) ) . '"
213
+ data-name="' . esc_attr( sprintf( __( '%s', 'woocommerce-gateway-stripe' ), get_bloginfo( 'name', 'display' ) ) ) . '"
214
+ data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '"
215
+ data-image="' . esc_attr( $this->stripe_checkout_image ) . '"
216
+ data-bitcoin="' . esc_attr( $this->bitcoin ? 'true' : 'false' ) . '"
217
+ data-locale="' . esc_attr( $this->stripe_checkout_locale ? $this->stripe_checkout_locale : 'en' ) . '">';
218
+
219
+ if ( $this->description ) {
220
+ echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $this->description ) ) );
221
+ }
222
+
223
+ if ( $display_tokenization ) {
224
+ $this->tokenization_script();
225
+ $this->saved_payment_methods();
226
+ }
227
+
228
+ if ( ! $this->stripe_checkout ) {
229
+ $this->form();
230
+
231
+ if ( $display_tokenization ) {
232
+ $this->save_payment_method_checkbox();
233
+ }
234
+ }
235
+
236
+ echo '</div>';
237
+ }
238
+
239
+ /**
240
+ * payment_scripts function.
241
+ *
242
+ * Outputs scripts used for stripe payment
243
+ *
244
+ * @access public
245
+ */
246
+ public function payment_scripts() {
247
+ if ( $this->stripe_checkout ) {
248
+ wp_enqueue_script( 'stripe', 'https://checkout.stripe.com/v2/checkout.js', '', '2.0', true );
249
+ wp_enqueue_script( 'woocommerce_stripe', plugins_url( 'assets/js/stripe_checkout.js', WC_STRIPE_MAIN_FILE ), array( 'stripe' ), WC_STRIPE_VERSION, true );
250
+ } else {
251
+ wp_enqueue_script( 'stripe', 'https://js.stripe.com/v2/', '', '1.0', true );
252
+ wp_enqueue_script( 'woocommerce_stripe', plugins_url( 'assets/js/stripe.js', WC_STRIPE_MAIN_FILE ), array( 'jquery-payment', 'stripe' ), WC_STRIPE_VERSION, true );
253
+ }
254
+
255
+ $stripe_params = array(
256
+ 'key' => $this->publishable_key,
257
+ 'i18n_terms' => __( 'Please accept the terms and conditions first', 'woocommerce-gateway-stripe' ),
258
+ 'i18n_required_fields' => __( 'Please fill in required checkout fields first', 'woocommerce-gateway-stripe' ),
259
+ );
260
+
261
+ // If we're on the pay page we need to pass stripe.js the address of the order.
262
+ if ( is_checkout_pay_page() && isset( $_GET['order'] ) && isset( $_GET['order_id'] ) ) {
263
+ $order_key = urldecode( $_GET['order'] );
264
+ $order_id = absint( $_GET['order_id'] );
265
+ $order = wc_get_order( $order_id );
266
+
267
+ if ( $order->id === $order_id && $order->order_key === $order_key ) {
268
+ $stripe_params['billing_first_name'] = $order->billing_first_name;
269
+ $stripe_params['billing_last_name'] = $order->billing_last_name;
270
+ $stripe_params['billing_address_1'] = $order->billing_address_1;
271
+ $stripe_params['billing_address_2'] = $order->billing_address_2;
272
+ $stripe_params['billing_state'] = $order->billing_state;
273
+ $stripe_params['billing_city'] = $order->billing_city;
274
+ $stripe_params['billing_postcode'] = $order->billing_postcode;
275
+ $stripe_params['billing_country'] = $order->billing_country;
276
+ }
277
+ }
278
+
279
+ wp_localize_script( 'woocommerce_stripe', 'wc_stripe_params', apply_filters( 'wc_stripe_params', $stripe_params ) );
280
+ }
281
+
282
+ /**
283
+ * Generate the request for the payment.
284
+ * @param WC_Order $order
285
+ * @param object $source
286
+ * @return array()
287
+ */
288
+ protected function generate_payment_request( $order, $source ) {
289
+ $post_data = array();
290
+ $post_data['currency'] = strtolower( $order->get_order_currency() ? $order->get_order_currency() : get_woocommerce_currency() );
291
+ $post_data['amount'] = $this->get_stripe_amount( $order->get_total(), $post_data['currency'] );
292
+ $post_data['description'] = sprintf( __( '%s - Order %s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() );
293
+ $post_data['capture'] = $this->capture ? 'true' : 'false';
294
+
295
+ if ( ! empty( $order->billing_email ) && apply_filters( 'wc_stripe_send_stripe_receipt', false ) ) {
296
+ $post_data['receipt_email'] = $order->billing_email;
297
+ }
298
+
299
+ $post_data['expand[]'] = 'balance_transaction';
300
+
301
+ if ( $source->customer ) {
302
+ $post_data['customer'] = $source->customer;
303
+ }
304
+
305
+ if ( $source->source ) {
306
+ $post_data['source'] = $source->source;
307
+ }
308
+
309
+ return $post_data;
310
+ }
311
+
312
+ /**
313
+ * Get payment source. This can be a new token or existing card.
314
+ * @param bool $force_customer Should we force customer creation?
315
+ * @return object
316
+ */
317
+ protected function get_source( $user_id, $force_customer = false ) {
318
+ $stripe_customer = new WC_Stripe_Customer( $user_id );
319
+ $stripe_source = false;
320
+ $token_id = false;
321
+
322
+ // New CC info was entered and we have a new token to process
323
+ if ( isset( $_POST['stripe_token'] ) ) {
324
+ $stripe_token = wc_clean( $_POST['stripe_token'] );
325
+ $maybe_saved_card = ! isset( $_POST['wc-stripe-new-payment-method'] ) || ! empty( $_POST['wc-stripe-new-payment-method'] );
326
+
327
+ // This is true if the user wants to store the card to their account.
328
+ if ( ( $user_id && $this->saved_cards && $maybe_saved_card ) || $force_customer ) {
329
+ $stripe_source = $stripe_customer->add_card( $stripe_token );
330
+
331
+ if ( is_wp_error( $stripe_source ) ) {
332
+ throw new Exception( $stripe_source->get_error_message() );
333
+ }
334
+
335
+ } else {
336
+ // Not saving token, so don't define customer either.
337
+ $stripe_source = $stripe_token;
338
+ $stripe_customer = false;
339
+ }
340
+ }
341
+
342
+ // Use an existing token, and then process the payment
343
+ elseif ( isset( $_POST['wc-stripe-payment-token'] ) && 'new' !== $_POST['wc-stripe-payment-token'] ) {
344
+ $token_id = wc_clean( $_POST['wc-stripe-payment-token'] );
345
+ $token = WC_Payment_Tokens::get( $token_id );
346
+
347
+ if ( ! $token || $token->get_user_id() !== get_current_user_id() ) {
348
+ WC()->session->set( 'refresh_totals', true );
349
+ throw new Exception( __( 'Invalid payment method. Please input a new card number.', 'woocommerce-gateway-stripe' ) );
350
+ }
351
+
352
+ $stripe_source = $token->get_token();
353
+ }
354
+
355
+ return (object) array(
356
+ 'token_id' => $token_id,
357
+ 'customer' => $stripe_customer ? $stripe_customer->get_id() : false,
358
+ 'source' => $stripe_source,
359
+ );
360
+ }
361
+
362
+ /**
363
+ * Get payment source from an order. This could be used in the future for
364
+ * a subscription as an example, therefore using the current user ID would
365
+ * not work - the customer won't be logged in :)
366
+ *
367
+ * Not using 2.6 tokens for this part since we need a customer AND a card
368
+ * token, and not just one.
369
+ *
370
+ * @param object $order
371
+ * @return object
372
+ */
373
+ protected function get_order_source( $order = null ) {
374
+ $stripe_customer = new WC_Stripe_Customer();
375
+ $stripe_source = false;
376
+ $token_id = false;
377
+
378
+ if ( $order ) {
379
+ if ( $meta_value = get_post_meta( $order->id, '_stripe_customer_id', true ) ) {
380
+ $stripe_customer->set_id( $meta_value );
381
+ }
382
+ if ( $meta_value = get_post_meta( $order->id, '_stripe_card_id', true ) ) {
383
+ $stripe_source = $meta_value;
384
+ }
385
+ }
386
+
387
+ return (object) array(
388
+ 'token_id' => $token_id,
389
+ 'customer' => $stripe_customer ? $stripe_customer->get_id() : false,
390
+ 'source' => $stripe_source,
391
+ );
392
+ }
393
+
394
+ /**
395
+ * Process the payment
396
+ */
397
+ public function process_payment( $order_id, $retry = true, $force_customer = false ) {
398
+ try {
399
+ $order = wc_get_order( $order_id );
400
+ $source = $this->get_source( get_current_user_id(), $force_customer );
401
+
402
+ if ( empty( $source->source ) && empty( $source->customer ) ) {
403
+ $error_msg = __( 'Please enter your card details to make a payment.', 'woocommerce-gateway-stripe' );
404
+ $error_msg .= ' ' . __( 'Developers: Please make sure that you are including jQuery and there are no JavaScript errors on the page.', 'woocommerce-gateway-stripe' );
405
+ throw new Exception( $error_msg );
406
+ }
407
+
408
+ // Store source to order meta
409
+ $this->save_source( $order, $source );
410
+
411
+ // Handle payment
412
+ if ( $order->get_total() > 0 ) {
413
+
414
+ if ( $order->get_total() * 100 < 50 ) {
415
+ throw new Exception( __( 'Sorry, the minimum allowed order total is 0.50 to use this payment method.', 'woocommerce-gateway-stripe' ) );
416
+ }
417
+
418
+ WC_Stripe::log( "Info: Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
419
+
420
+ // Make the request
421
+ $response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source ) );
422
+
423
+ if ( is_wp_error( $response ) ) {
424
+ // Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without.
425
+ if ( 'customer' === $response->get_error_code() && $retry ) {
426
+ delete_user_meta( get_current_user_id(), '_stripe_customer_id' );
427
+ return $this->process_payment( $order_id, false, $force_customer );
428
+ // Source param wrong? The CARD may have been deleted on stripe's end. Remove token and show message.
429
+ } elseif ( 'source' === $response->get_error_code() && $source->token_id ) {
430
+ $token = WC_Payment_Tokens::get( $source->token_id );
431
+ $token->delete();
432
+ throw new Exception( __( 'This card is no longer available and has been removed.', 'woocommerce-gateway-stripe' ) );
433
+ }
434
+ throw new Exception( $response->get_error_code() . ': ' . $response->get_error_message() );
435
+ }
436
+
437
+ // Process valid response
438
+ $this->process_response( $response, $order );
439
+ } else {
440
+ $order->payment_complete();
441
+ }
442
+
443
+ // Remove cart
444
+ WC()->cart->empty_cart();
445
+
446
+ // Return thank you page redirect
447
+ return array(
448
+ 'result' => 'success',
449
+ 'redirect' => $this->get_return_url( $order )
450
+ );
451
+
452
+ } catch ( Exception $e ) {
453
+ wc_add_notice( $e->getMessage(), 'error' );
454
+ WC()->session->set( 'refresh_totals', true );
455
+ WC_Stripe::log( sprintf( __( 'Error: %s', 'woocommerce-gateway-stripe' ), $e->getMessage() ) );
456
+ return;
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Save source to order.
462
+ */
463
+ protected function save_source( $order, $source ) {
464
+ // Store source in the order
465
+ if ( $source->customer ) {
466
+ update_post_meta( $order->id, '_stripe_customer_id', $source->customer );
467
+ }
468
+ if ( $source->source ) {
469
+ update_post_meta( $order->id, '_stripe_card_id', $source->source );
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Store extra meta data for an order from a Stripe Response.
475
+ */
476
+ public function process_response( $response, $order ) {
477
+ WC_Stripe::log( "Processing response: " . print_r( $response, true ) );
478
+
479
+ // Store charge data
480
+ update_post_meta( $order->id, '_stripe_charge_id', $response->id );
481
+ update_post_meta( $order->id, '_stripe_charge_captured', $response->captured ? 'yes' : 'no' );
482
+
483
+ // Store other data such as fees
484
+ if ( isset( $response->balance_transaction ) && isset( $response->balance_transaction->fee ) ) {
485
+ $fee = number_format( $response->balance_transaction->fee / 100, 2, '.', '' );
486
+ update_post_meta( $order->id, 'Stripe Fee', $fee );
487
+ update_post_meta( $order->id, 'Net Revenue From Stripe', $order->get_total() - $fee );
488
+ }
489
+
490
+ if ( $response->captured ) {
491
+ $order->payment_complete( $response->id );
492
+ WC_Stripe::log( "Successful charge: $response->id" );
493
+ } else {
494
+ add_post_meta( $order->id, '_transaction_id', $response->id, true );
495
+
496
+ if ( $order->has_status( array( 'pending', 'failed' ) ) ) {
497
+ $order->reduce_order_stock();
498
+ }
499
+
500
+ $order->update_status( 'on-hold', sprintf( __( 'Stripe charge authorized (Charge ID: %s). Process order to take payment, or cancel to remove the pre-authorization.', 'woocommerce-gateway-stripe' ), $response->id ) );
501
+ WC_Stripe::log( "Successful auth: $response->id" );
502
+ }
503
+
504
+ return $response;
505
+ }
506
+
507
+ /**
508
+ * Add payment method via account screen.
509
+ * We don't store the token locally, but to the Stripe API.
510
+ * @since 3.0.0
511
+ */
512
+ public function add_payment_method() {
513
+ if ( empty( $_POST['stripe_token'] ) || ! is_user_logged_in() ) {
514
+ wc_add_notice( __( 'There was a problem adding the card.', 'woocommerce-gateway-stripe' ), 'error' );
515
+ return;
516
+ }
517
+
518
+ $stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
519
+ $card = $stripe_customer->add_card( wc_clean( $_POST['stripe_token'] ) );
520
+
521
+ if ( is_wp_error( $card ) ) {
522
+ throw new Exception( $card->get_error_message() );
523
+ }
524
+
525
+ return array(
526
+ 'result' => 'success',
527
+ 'redirect' => wc_get_endpoint_url( 'payment-methods' ),
528
+ );
529
+ }
530
+
531
+ /**
532
+ * Refund a charge
533
+ * @param int $order_id
534
+ * @param float $amount
535
+ * @return bool
536
+ */
537
+ public function process_refund( $order_id, $amount = null, $reason = '' ) {
538
+ $order = wc_get_order( $order_id );
539
+
540
+ if ( ! $order || ! $order->get_transaction_id() ) {
541
+ return false;
542
+ }
543
+
544
+ $body = array();
545
+
546
+ if ( ! is_null( $amount ) ) {
547
+ $body['amount'] = $this->get_stripe_amount( $amount );
548
+ }
549
+
550
+ if ( $reason ) {
551
+ $body['metadata'] = array(
552
+ 'reason' => $reason,
553
+ );
554
+ }
555
+
556
+ WC_Stripe::log( "Info: Beginning refund for order $order_id for the amount of {$amount}" );
557
+
558
+ $response = WC_Stripe_API::request( $body, 'charges/' . $order->get_transaction_id() . '/refunds' );
559
+
560
+ if ( is_wp_error( $response ) ) {
561
+ WC_Stripe::log( "Error: " . $response->get_error_message() );
562
+ return $response;
563
+ } elseif ( ! empty( $response->id ) ) {
564
+ $refund_message = sprintf( __( 'Refunded %s - Refund ID: %s - Reason: %s', 'woocommerce-gateway-stripe' ), wc_price( $response->amount / 100 ), $response->id, $reason );
565
+ $order->add_order_note( $refund_message );
566
+ WC_Stripe::log( "Success: " . html_entity_decode( strip_tags( $refund_message ) ) );
567
+ return true;
568
+ }
569
+ }
570
+ }
includes/class-wc-stripe-api.php ADDED
@@ -0,0 +1,91 @@
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit;
4
+ }
5
+
6
+ /**
7
+ * WC_Stripe_API class.
8
+ *
9
+ * Communicates with Stripe API.
10
+ */
11
+ class WC_Stripe_API {
12
+
13
+ /**
14
+ * Stripe API Endpoint
15
+ */
16
+ const ENDPOINT = 'https://api.stripe.com/v1/';
17
+
18
+ /**
19
+ * Secret API Key.
20
+ * @var string
21
+ */
22
+ private static $secret_key = '';
23
+
24
+ /**
25
+ * Set secret API Key.
26
+ * @param string $key
27
+ */
28
+ public static function set_secret_key( $secret_key ) {
29
+ self::$secret_key = $secret_key;
30
+ }
31
+
32
+ /**
33
+ * Get secret key.
34
+ * @return string
35
+ */
36
+ public static function get_secret_key() {
37
+ if ( ! self::$secret_key ) {
38
+ $options = get_option( 'woocommerce_stripe_settings' );
39
+
40
+ if ( isset( $options['testmode'], $options['secret_key'], $options['test_secret_key'] ) ) {
41
+ self::set_secret_key( 'yes' === $options['testmode'] ? $options['test_secret_key'] : $options['secret_key'] );
42
+ }
43
+ }
44
+ return self::$secret_key;
45
+ }
46
+
47
+ /**
48
+ * Send the request to Stripe's API
49
+ *
50
+ * @param array $request
51
+ * @param string $api
52
+ * @return array|WP_Error
53
+ */
54
+ public static function request( $request, $api = 'charges', $method = 'POST' ) {
55
+ WC_Stripe::log( "{$api} request: " . print_r( $request, true ) );
56
+
57
+ $response = wp_safe_remote_post(
58
+ self::ENDPOINT . $api,
59
+ array(
60
+ 'method' => $method,
61
+ 'headers' => array(
62
+ 'Authorization' => 'Basic ' . base64_encode( self::get_secret_key(). ':' ),
63
+ 'Stripe-Version' => '2016-03-07'
64
+ ),
65
+ 'body' => apply_filters( 'woocommerce_stripe_request_body', $request, $api ),
66
+ 'timeout' => 70,
67
+ 'user-agent' => 'WooCommerce ' . WC()->version
68
+ )
69
+ );
70
+
71
+ if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
72
+ WC_Stripe::log( "Error Response: " . print_r( $response, true ) );
73
+ return new WP_Error( 'stripe_error', __( 'There was a problem connecting to the payment gateway.', 'woocommerce-gateway-stripe' ) );
74
+ }
75
+
76
+ $parsed_response = json_decode( $response['body'] );
77
+ // Handle response
78
+ if ( ! empty( $parsed_response->error ) ) {
79
+ if ( ! empty( $parsed_response->error->param ) ) {
80
+ $code = $parsed_response->error->param;
81
+ } elseif ( ! empty( $parsed_response->error->code ) ) {
82
+ $code = $parsed_response->error->code;
83
+ } else {
84
+ $code = 'stripe_error';
85
+ }
86
+ return new WP_Error( $code, $parsed_response->error->message );
87
+ } else {
88
+ return $parsed_response;
89
+ }
90
+ }
91
+ }
includes/class-wc-stripe-customer.php ADDED
@@ -0,0 +1,281 @@
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ exit;
4
+ }
5
+
6
+ /**
7
+ * WC_Stripe_Customer class.
8
+ *
9
+ * Represents a Stripe Customer.
10
+ */
11
+ class WC_Stripe_Customer {
12
+
13
+ /**
14
+ * Stripe customer ID
15
+ * @var string
16
+ */
17
+ private $id = '';
18
+
19
+ /**
20
+ * WP User ID
21
+ * @var integer
22
+ */
23
+ private $user_id = 0;
24
+
25
+ /**
26
+ * Data from API
27
+ * @var array
28
+ */
29
+ private $customer_data = array();
30
+
31
+ /**
32
+ * Constructor
33
+ * @param integer $user_id
34
+ */
35
+ public function __construct( $user_id = 0 ) {
36
+ if ( $user_id ) {
37
+ $this->set_user_id( $user_id );
38
+ $this->set_id( get_user_meta( $user_id, '_stripe_customer_id', true ) );
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Get Stripe customer ID.
44
+ * @return string
45
+ */
46
+ public function get_id() {
47
+ return $this->id;
48
+ }
49
+
50
+ /**
51
+ * Set Stripe customer ID.
52
+ * @param [type] $id [description]
53
+ */
54
+ public function set_id( $id ) {
55
+ $this->id = wc_clean( $id );
56
+ }
57
+
58
+ /**
59
+ * User ID in WordPress.
60
+ * @return int
61
+ */
62
+ public function get_user_id() {
63
+ return absint( $this->user_id );
64
+ }
65
+
66
+ /**
67
+ * Set User ID used by WordPress.
68
+ * @param int $user_id
69
+ */
70
+ public function set_user_id( $user_id ) {
71
+ $this->user_id = absint( $user_id );
72
+ }
73
+
74
+ /**
75
+ * Get user object.
76
+ * @return WP_User
77
+ */
78
+ protected function get_user() {
79
+ return $this->get_user_id() ? get_user_by( 'id', $this->get_user_id() ) : false;
80
+ }
81
+
82
+ /**
83
+ * Store data from the Stripe API about this customer
84
+ */
85
+ public function set_customer_data( $data ) {
86
+ $this->customer_data = $data;
87
+ }
88
+
89
+ /**
90
+ * Get data from the Stripe API about this customer
91
+ */
92
+ public function get_customer_data() {
93
+ if ( empty( $this->customer_data ) && $this->get_id() && false === ( $this->customer_data = get_transient( 'stripe_customer_' . $this->get_id() ) ) ) {
94
+ $response = WC_Stripe_API::request( array(), 'customers/' . $this->get_id() );
95
+
96
+ if ( ! is_wp_error( $response ) ) {
97
+ $this->set_customer_data( $response );
98
+ set_transient( 'stripe_customer_' . $this->get_id(), $response, HOUR_IN_SECONDS * 48 );
99
+ }
100
+ }
101
+ return $this->customer_data;
102
+ }
103
+
104
+ /**
105
+ * Get default card/source
106