├── README.md ├── callback └── stripe.php ├── ccpay.php ├── clientareacreditcard-stripe.tpl └── stripe.php /README.md: -------------------------------------------------------------------------------- 1 | WHMCS Custom Stripe Payment Gateway 2 | ============ 3 | 4 | This is a free and open source Stripe Payment Gateway for WHMCS that supports one time and recurring payments without ever having a credit card number hit your WHMCS server. It's pretty neat! 5 | 6 | ## Overview 7 | 8 | This gateway allows the [WHMCS](http://www.whmcs.com) billing system to use [Stripe's](https://www.stripe.com) one time and reoccurring payment gateway capabilities. [Stripe](https://www.stripe.com) provides the unique ability to take a client's credit card information without ever having the client leave your site, but it also allows for credit card data to never get stored in or even pass through your own server, eliminating costly concerns over PCI compliance. This gateway for WHMCS is being released free for anyone, although it should still be considered in beta as there are likely still bugs to work out of it. 9 | 10 | ## Instructions For Use 11 | 12 | 1. Download the [Stripe library for PHP](https://github.com/stripe/stripe-php). Create a new folder called `stripe` within the `/modules/gateways/` directory of your WHMCS installation and copy the contents of the `lib` folder (from your download) into that newly created folder. 13 | 2. Copy the file in the repository's root folder called `stripe.php` and place it into the `/modules/gateways/` folder of your WHMCS installation. 14 | 3. From within the `callback` folder of the repository, copy the other `stripe.php` file and place it into the `/modules/gateways/callback` folder of your WHMCS installation. 15 | 4. Copy the `ccpay.php` file from the repository into the root directory of your WHMCS installation. 16 | 5. Finally, copy the `clientareacreditcard-stripe.tpl` file into the root level of the theme folder you are currently using for WHMCS. For example, if you're using the `default` theme, then copy this file to `/templates/default/`. 17 | 6. Add a webhook in Stripe to `https://yourwhmcsinstall.com/modules/gateways/callback/stripe.php`. 18 | 19 | In the end, your folder structure should look roughly like the diagram below, with a ccpay.php file in the root of your install, a stripe.php in `/modules/gateways/callback`, a stripe.php in `/modules/gateways/`, a `clientareacreditcard-stripe.tpl` in the root of your WHMCS active template folder, and the `lib` folder of the Stripe API in the newly-created `/modules/gateways/stripe/` folder. 20 | 21 | ``` 22 | whmcs 23 | |-- ccpay.php 24 | |-- modules 25 | |-- gateways 26 | |-- callbacks 27 | |-- stripe.php 28 | |-- stripe (contains the lib folder of the Stripe PHP API) 29 | |-- Stripe 30 | |-- ... (files from Stripe API) 31 | |-- data 32 | |-- ... (files from Stripe API) 33 | |-- Stripe.php (Stripe API core file) 34 | |-- stripe.php 35 | |-- templates 36 | |-- yourtemplatename (i.e. default) 37 | |-- clientareacreditcard-stripe.tpl 38 | ``` 39 | 40 | You may now activate this new payment gateway from within WHMCS through the Setup > Payments > Payment Gateways screen. This module should be listed as *Stripe*. You can then fill in the appropriate API key information as well as a *required* email address where the gateway can send you any serious errors that come up while billing clients or communicating with Stripe. 41 | 42 | ## Warnings and Notices 43 | 44 | + This payment gateway relies on the way it saves an Invoice title and description to Stripe in order to properly function and credit invoices once they are paid. Because of this, you shouldn't attempt to modify the way invoices and plans are displayed in Stripe's web interface. 45 | + This payment gateway assumes that you are using the default WHMCS template based on Twitter's Bootstrap framework. If you are not, or if your theme uses a heavily or otherwise extremely modified version of Bootstrap, double-check to ensure that the Javscript this module uses will still work properly. 46 | + This gateway currently only works in English with United States Dollars as the currency. 47 | 48 | ## Credits and Acknowledgements 49 | 50 | This module wouldn't have been possible to make without the help of [WHMCS'](http://www.whmcs.com) developer team as well as the staff at [Stripe](https://www.stripe.com) for their great product, well-documented PHP API, and quick support as well. All pieces of code, including the payment gateway template, are the property of their original owners. 51 | 52 | ## Support Information 53 | 54 | I'm always looking to improve this code, so if you see something that can be changed or if you have an idea for a new feature or any other feedback, send me an email to `support *at* next gen web media dot com`, or send me a message on Twitter (`@grantdtech`), and I'll get right back to you. If you decide to use this module in your WHMCS install, send me a message to say hello (and let me know what you think too) and it'll make my day. Thanks! -------------------------------------------------------------------------------- /callback/stripe.php: -------------------------------------------------------------------------------- 1 | id; 26 | 27 | try { 28 | 29 | $event = Stripe_Event::retrieve($event_id); 30 | 31 | if($event->type == 'charge.succeeded') { 32 | 33 | // Pull invoice ID from Stripe description 34 | if ($event->data->object->invoice != "") { // This is an invoice/subscription payment, get the WHMCS invoice ID 35 | $invoice_id = $event->data->object->invoice; 36 | $retrieved_invoice = Stripe_Invoice::retrieve($invoice_id)->lines->all(array('count'=>1, 'offset'=>0)); 37 | $description_invoice = $retrieved_invoice["data"][0]["plan"]["name"]; 38 | $description = $description_invoice; 39 | } else { // This is a one time payment 40 | $description = $event->data->object->description; 41 | } 42 | 43 | // Get the invoice ID from the transaction 44 | $start = strpos($description, "#") + strlen("#"); 45 | $end = strpos($description, " ", $start); 46 | $invoiceid = substr($description, $start, $end - $start); 47 | 48 | $transid = $event->data->object->id; 49 | 50 | $amount_cents = $event->data->object->amount; 51 | $amount = $amount_cents / 100; 52 | 53 | $fee_cents = floatval($event->data->object->fee); 54 | $fee = $fee_cents / 100; 55 | 56 | $paid = $event->data->object->paid; 57 | 58 | } 59 | 60 | } catch (Exception $e) { 61 | mail($gateway["problememail"],"Stripe Failed Callback","A problem prevented Stripe from properly processing an incoming payment webhook:" . $e); 62 | } 63 | 64 | $invoiceid = checkCbInvoiceID($invoiceid,$GATEWAY["name"]); # Checks invoice ID is a valid invoice number or ends processing 65 | 66 | checkCbTransID($transid); # Checks transaction number isn't already in the database and ends processing if it does 67 | 68 | if ($paid == true) { 69 | 70 | # Successful 71 | addInvoicePayment($invoiceid,$transid,$amount,$fee,$gatewaymodule); # Apply Payment to Invoice: invoiceid, transactionid, amount paid, fees, modulename 72 | logTransaction($GATEWAY["name"],$event,"Successful"); # Save to Gateway Log: name, data array, status 73 | } else { 74 | # Unsuccessful 75 | logTransaction($GATEWAY["name"],$event,"Unsuccessful"); # Save to Gateway Log: name, data array, status 76 | } 77 | 78 | ?> -------------------------------------------------------------------------------- /ccpay.php: -------------------------------------------------------------------------------- 1 | $" . $amount . "."; 81 | 82 | if ($_POST['stripeToken'] != "") { 83 | 84 | $token = $_POST['stripeToken']; 85 | $amount_cents = str_replace(".","",$amount); 86 | $description = "Invoice #" . $smartyvalues["invoiceid"] . " - " . $email; 87 | 88 | try { 89 | 90 | $charge = Stripe_Charge::create(array( 91 | "amount" => $amount_cents, 92 | "currency" => "usd", 93 | "card" => $token, 94 | "description" => $description) 95 | ); 96 | 97 | if ($charge->card->address_zip_check == "fail") { 98 | throw new Exception("zip_check_invalid"); 99 | } else if ($charge->card->address_line1_check == "fail") { 100 | throw new Exception("address_check_invalid"); 101 | } else if ($charge->card->cvc_check == "fail") { 102 | throw new Exception("cvc_check_invalid"); 103 | } 104 | 105 | // Payment has succeeded, no exceptions were thrown or otherwise caught 106 | $smartyvalues["success"] = true; 107 | 108 | 109 | } catch(Stripe_CardError $e) { 110 | 111 | $error = $e->getMessage(); 112 | $smartyvalues["processingerror"] = 'Error: ' . $error . '.'; 113 | 114 | } catch (Stripe_InvalidRequestError $e) { 115 | 116 | } catch (Stripe_AuthenticationError $e) { 117 | send_error("authentication",$e); 118 | } catch (Stripe_ApiConnectionError $e) { 119 | send_error("network", $e); 120 | } catch (Stripe_Error $e) { 121 | send_error("generic", $e); 122 | } catch (Exception $e) { 123 | 124 | if ($e->getMessage() == "zip_check_invalid") { 125 | $smartyvalues["processingerror"] = 'Error: The address information on your account does not match that of the credit card you are trying to use. Please try again or contact us if the problem persists.'; 126 | } else if ($e->getMessage() == "address_check_invalid") { 127 | $smartyvalues["processingerror"] = 'The address information on your account does not match that of the credit card you are trying to use. Please try again or contact us if the problem persists.'; 128 | } else if ($e->getMessage() == "cvc_check_invalid") { 129 | $smartyvalues["processingerror"] = 'The credit card information you specified is not valid. Please try again or contact us if the problem persists.'; 130 | } else { 131 | send_error("unknown", $e); 132 | } 133 | 134 | } 135 | 136 | } // end of if to check if this is a token acceptance for otps 137 | 138 | } else { // end if to check if this is a one time payment. else = this IS a otp 139 | 140 | $amount_total = $_POST['total_amount']; 141 | $amount_subscribe = $_POST['amount']; 142 | $amount_diff = abs($amount_total - $amount_subscribe); 143 | 144 | if ($multiple == "true") { 145 | $smartyvalues['explanation'] = "You are about to set up a $" . $amount_subscribe . " charge that will automatically bill to your credit card every month. You are also going to pay a one time charge of $" . $amount_diff . "."; 146 | } else { 147 | $smartyvalues['explanation'] = "You are about to set up a $" . $amount_subscribe . " charge that will automatically bill to your credit card every month."; 148 | } 149 | 150 | if ($_POST['stripeToken'] != "") { 151 | 152 | $token = $_POST['stripeToken']; 153 | $multiple = $_POST['multiple']; 154 | 155 | $amount_total_cents = $amount_total * 100; 156 | $amount_subscribe_cents = $amount_subscribe * 100; 157 | $amount_diff_cents = $amount_diff * 100; 158 | 159 | $message = "Amount Total: " . $amount_total . "
"; 160 | $message .= "Amount Subscribe: " . $amount_subscribe . "
"; 161 | $message .= "Amount Difference (OTP): " . $amount_diff . "
"; 162 | $message .= "Amount Difference (OTP) in Cents: " . $amount_diff_cents . "
"; 163 | 164 | $ng_plan_name = $_POST['planname']; 165 | $ng_plan_id = $_POST['planid']; 166 | $description_otp = "Invoice #" . $smartyvalues["invoiceid"] . " - " . $email . " - One Time Services"; 167 | $stripe_plan_name = "Invoice #" . $smartyvalues['invoiceid'] . ' - ' . $ng_plan_name . ' - ' . $email; 168 | 169 | // Create "custom" plan for this user 170 | try { 171 | Stripe_Plan::create(array( 172 | "amount" => $amount_subscribe_cents, 173 | "interval" => "month", 174 | "name" => $stripe_plan_name, 175 | "currency" => "usd", 176 | "id" => $ng_plan_id 177 | )); 178 | 179 | 180 | // Find out if this customer already has a paying item with stripe and if they have a subscription with it 181 | $current_uid = $_SESSION['uid']; 182 | $q = mysql_query("SELECT subscriptionid FROM tblhosting WHERE userid='" . $current_uid . "' AND paymentmethod='stripe' AND subscriptionid !=''"); 183 | if (mysql_num_rows($q) > 0) { 184 | $data = mysql_fetch_array($q); 185 | $stripe_customer_id = $data[0]; 186 | } else { 187 | $stripe_customer_id = ""; 188 | } 189 | 190 | if ($stripe_customer_id == "") { 191 | $customer = Stripe_Customer::create(array( // Sign them up for the requested plan and add the customer id into the subscription id 192 | "card" => $token, 193 | "plan" => $ng_plan_id, 194 | "email" => $email 195 | )); 196 | $cust_id = $customer->id; 197 | $q = mysql_query("UPDATE tblhosting SET subscriptionid='" . $cust_id . "' WHERE id='" . $ng_plan_id . "'"); 198 | } else { // Create the customer from scratch 199 | $c = Stripe_Customer::retrieve($stripe_customer_id); 200 | $c->updateSubscription(array("plan" => "basic", "prorate" => false)); 201 | } 202 | 203 | if ($customer->card->address_zip_check == "fail") { 204 | throw new Exception("zip_check_invalid"); 205 | } else if ($charge->card->address_line1_check == "fail") { 206 | throw new Exception("address_check_invalid"); 207 | } else if ($charge->card->cvc_check == "fail") { 208 | throw new Exception("cvc_check_invalid"); 209 | } 210 | 211 | if ($multiple == "true") { // Bill the customer once for other items they have too 212 | $charge = Stripe_Charge::create(array( 213 | "amount" => $amount_diff_cents, 214 | "currency" => "usd", 215 | "customer" => $cust_id, 216 | "description" => $description_otp 217 | )); 218 | 219 | if ($charge->card->address_zip_check == "fail") { 220 | throw new Exception("zip_check_invalid"); 221 | } else if ($charge->card->address_line1_check == "fail") { 222 | throw new Exception("address_check_invalid"); 223 | } else if ($charge->card->cvc_check == "fail") { 224 | throw new Exception("cvc_check_invalid"); 225 | } 226 | 227 | } 228 | 229 | // Payment has succeeded, no exceptions were thrown or otherwise caught 230 | $smartyvalues["success"] = true; 231 | 232 | } catch(Stripe_CardError $e) { 233 | 234 | $error = $e->getMessage(); 235 | $smartyvalues["processingerror"] = 'Error: ' . $error . '.'; 236 | 237 | } catch (Stripe_InvalidRequestError $e) { 238 | 239 | } catch (Stripe_AuthenticationError $e) { 240 | send_error("authentication",$e); 241 | } catch (Stripe_ApiConnectionError $e) { 242 | send_error("network", $e); 243 | } catch (Stripe_Error $e) { 244 | send_error("generic", $e); 245 | } catch (Exception $e) { 246 | 247 | if ($e->getMessage() == "zip_check_invalid") { 248 | $smartyvalues["processingerror"] = 'Error: The address information on your account does not match that of the credit card you are trying to use. Please try again or contact us if the problem persists.'; 249 | } else if ($e->getMessage() == "address_check_invalid") { 250 | $smartyvalues["processingerror"] = 'The address information on your account does not match that of the credit card you are trying to use. Please try again or contact us if the problem persists.'; 251 | } else if ($e->getMessage() == "cvc_check_invalid") { 252 | $smartyvalues["processingerror"] = 'The credit card information you specified is not valid. Please try again or contact us if the problem persists.'; 253 | } else { 254 | send_error("unkown", $e); 255 | } 256 | 257 | } 258 | 259 | } // end of if to check if this is a token acceptance for recurs 260 | 261 | } 262 | 263 | } else { // User is logged in but they shouldn't be here (i.e. they weren't here from an invoice) 264 | 265 | header("Location: clientarea.php?action=details"); 266 | 267 | } 268 | 269 | } else { 270 | 271 | header("Location: index.php"); 272 | 273 | } 274 | 275 | # Define the template filename to be used without the .tpl extension 276 | 277 | $templatefile = "clientareacreditcard-stripe"; 278 | 279 | outputClientArea($templatefile); 280 | 281 | ?> -------------------------------------------------------------------------------- /clientareacreditcard-stripe.tpl: -------------------------------------------------------------------------------- 1 | {include file="$template/pageheader.tpl" title="Credit Card Payment Information"} 2 | 3 | {if $success != true} 4 | 5 | 6 | 57 | 58 | {if $processingerror} 59 |
60 |

{$processingerror}

61 |
62 | {/if} 63 | 64 | 67 | 68 |

{$explanation} Please make sure the credit card billing information below is correct before continuing and then click Pay Now.

69 | 70 |
71 |
72 | 73 |
74 | 75 |

Cardholder Information

76 | 77 |
78 | 79 |
80 | 81 |
82 |
83 | 84 |
85 | 86 |
87 |

88 | 89 |
90 |
91 | 92 |
93 | 94 |
95 | 96 |
97 |
98 | 99 |
100 | 101 |
102 | 103 |
104 |
105 | 106 |
107 | 108 |
109 | 110 |
111 |
112 | 113 |

Card Information

114 | 115 |
116 | 117 |
118 | 119 |
120 |
121 | 122 |
123 | 124 |
125 | 126 |
127 |
128 | 129 |
130 | 131 |
132 | / 133 |
134 |
135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 |
147 | 148 |
149 | 150 | Cancel Payment 151 |
152 | 153 |
154 | 155 | {/if} 156 | {if $success == true} 157 | 158 |
159 |

Success

160 |

Your credit card payment was successful.

161 |

Click here to view your paid invoice.

162 |
163 |
164 |
165 |
166 |
167 | {/if} 168 | 169 |
{$companyname} values the security of your personal information.
Credit card details are transmitted and stored according the highest level of security standards available.
170 | 171 |
-------------------------------------------------------------------------------- /stripe.php: -------------------------------------------------------------------------------- 1 | array("Type" => "System", "Value"=>"Stripe"), 6 | "public_live_key" => array("FriendlyName" => "Live Publishable Key", "Type" => "text", "Size" => "20", "Description" => "Available from Stripe's website at this link.", ), 7 | "private_live_key" => array("FriendlyName" => "Live Secret Key", "Type" => "text", "Size" => "20", "Description" => "Available from Stripe's website at this link.", ), 8 | "public_test_key" => array("FriendlyName" => "Test Publishable Key", "Type" => "text", "Size" => "20", "Description" => "Available from Stripe's website at this link.", ), 9 | "private_test_key" => array("FriendlyName" => "Test Secret Key", "Type" => "text", "Size" => "20", "Description" => "Available from Stripe's website at this link." , ), 10 | "problememail" => array("FriendlyName" => "Problem Report Email", "Type" => "text", "Size" => "20", "Description" => "Enter an email that the gateway can send a message to should an alert or other serious processing problem arise.", ), 11 | "testmode" => array("FriendlyName" => "Test Mode", "Type" => "yesno", "Description" => "Tick this to make all transactions use your test keys above.", ), 12 | ); 13 | return $configarray; 14 | } 15 | 16 | function stripe_link($params) { 17 | 18 | # Invoice Variables 19 | $invoiceid = $params['invoiceid']; 20 | $description = $params["description"]; 21 | $amount = $params['amount']; # Format: ##.## 22 | 23 | // Perform lookup to see if the invoice is for a recurring service 24 | $result = mysql_query("SELECT relid, amount, description FROM tblinvoiceitems WHERE type='Hosting' AND invoiceid='" . $invoiceid . "'"); 25 | $grab_relid = mysql_fetch_row($result); 26 | $relid = $grab_relid[0]; 27 | $subscribe_price = $grab_relid[1]; 28 | $plan_name = $grab_relid[2]; 29 | 30 | if ($relid != 0 || $relid != "") { 31 | $wording = "Pay Once"; 32 | } else { 33 | $wording = "Pay Now"; 34 | } 35 | 36 | if ($subscribe_price != $amount) { // Tell the payment processing page that they user needs to come back and pay their one time fees, such as a domain name, that were not covered as part of the subscription 37 | $warning = "true"; 38 | } else { 39 | $warning = "false"; 40 | } 41 | 42 | # Enter your code submit to the gateway... 43 | $code = '
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
'; 52 | 53 | if ($relid != 0 || $relid != "") { 54 | $code .= '
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
'; 66 | } 67 | 68 | return $code; 69 | 70 | } 71 | 72 | function stripe_refund($params) { 73 | 74 | require_once('stripe/Stripe.php'); 75 | 76 | $gatewaytestmode = $params["testmode"]; 77 | 78 | if ($gatewaytestmode == "on") { 79 | Stripe::setApiKey($params['private_test_key']); 80 | } else { 81 | Stripe::setApiKey($params['private_live_key']); 82 | } 83 | 84 | # Invoice Variables 85 | $transid = $params['transid']; 86 | 87 | # Perform Refund 88 | try { 89 | $ch = Stripe_Charge::retrieve($transid); 90 | $ch->refund(); 91 | return array("status"=>"success","transid"=>$ch["id"],"rawdata"=>$ch); 92 | } catch (Exception $e) { 93 | $response['error'] = $e->getMessage(); 94 | return array("status"=>"error","rawdata"=>$response['error']); 95 | } 96 | 97 | } 98 | 99 | ?> --------------------------------------------------------------------------------