├── 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 |
{$processingerror}
61 |{$explanation} Please make sure the credit card billing information below is correct before continuing and then click Pay Now.
69 | 70 | 154 | 155 | {/if} 156 | {if $success == true} 157 | 158 |Your credit card payment was successful.
161 |Click here to view your paid invoice.
162 |