1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 16:22:43 +01:00

For discussion -- Stripe integration

Summary:
various stripe stuff, including

- external stripe library
- payment form
- test controller to play with payment form, sample business logic

My main questions / discussion topics are...

- is the stripe PHP library too big? (ie should I write something more simple just for phabricator?)
-- if its cool, what is the best way to include the client? (ie should I make it a submodule rather than the flat copy here?)
- is the JS I wrote (too) ridiculous?
-- particularly unhappy with the error message stuff being in JS *but* it seemed the best choice given the most juicy error messages come from the stripe JS such that the overall code complexity is lowest this way.
- how should the stripe JS be included?
-- flat copy like I did here?
-- some sort of external?
-- can we just load it off stripe servers at request time? (I like that from the "if stripe is down, stripe is down" perspective)
- wasn't sure if the date control was too silly and should just be baked into the form?
-- for some reason I feel like its good to be prepared to walk away from Stripe / switch providers here, though I think this is on the wrong side of pragmatic

Test Plan: - played around with sample client form

Reviewers: epriestley

Reviewed By: epriestley

CC: aran

Differential Revision: https://secure.phabricator.com/D2096
This commit is contained in:
Bob Trahan 2012-04-04 16:09:29 -07:00
parent 877cb136e8
commit cc586b0afa
42 changed files with 5710 additions and 8 deletions

17
externals/stripe-js/stripe_core.js vendored Normal file
View file

@ -0,0 +1,17 @@
/**
* @provides stripe-core
* @do-not-minify
*/
(function(c){function k(a){return a.replace(/^\s+|\s+$/g,"")}function n(){if(!c.publishableKey)throw"No Publishable API Key: Call Stripe.setPublishableKey to provide your key.";}var d=null,l={};typeof window!=="undefined"&&!window.JSON&&(window.JSON={});(function(){if(typeof JSON.parse!=="function")JSON.parse=function(a,b){function d(a,e){var c,h,f=a[e];if(f&&typeof f==="object")for(c in f)Object.hasOwnProperty.call(f,c)&&(h=d(f,c),h!==void 0?f[c]=h:delete f[c]);return b.call(a,e,f)}var e=RegExp("[\\u0000\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]",
"g"),a=String(a);e.lastIndex=0;e.test(a)&&(a=a.replace(e,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)}));if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return e=eval("("+a+")"),typeof b==="function"?d({"":e},""):e;throw new SyntaxError("JSON.parse");}})();var v=function(a){function b(){d=null;var a=document.getElementsByTagName("body")[0],
b=document.createElement("iframe");m="stripeFrame"+(new Date).getTime();q=j.apiURL+"/js/v1/apitunnel.html";var i=q+"#"+encodeURIComponent(window.location.href);b.setAttribute("src",i);b.setAttribute("name",m);b.setAttribute("id",m);b.setAttribute("frameborder","0");b.setAttribute("scrolling","no");b.setAttribute("allowtransparency","true");b.setAttribute("width",0);b.setAttribute("height",0);b.setAttribute("style","position:absolute;top:0;left:0;width:0;height:0");i=function(){d=window.frames[m];
c()};b.attachEvent?b.attachEvent("onload",i):b.onload=i;a.appendChild(b)}function c(){if(d){var b=o.length;if(b>0){for(var e=0;e<b;++e){var i=o[e].message,r=i.id;h[r]=o[e].callback;a.postMessage(i,j.apiURL,q,d);f[r]=window.setTimeout(function(a){h[a](504,{error:{message:"There was an error processing your card"}});delete h[a];delete f[a]},6E4,r)}o=[]}}}if(typeof a==="undefined"){var a={},e=function(a){if(typeof a==="undefined"){var a={},b=function(){var a={};a.serialize=function(b,e){var d=[],c;for(c in b)if(b.hasOwnProperty(c)){var u=
e?e+"["+c+"]":c,f=b[c];d.push(typeof f=="object"?a.serialize(f,u):encodeURIComponent(u)+"="+encodeURIComponent(f))}return d.join("&")};a.deserialize=function(a){for(var b={},a=a.split("&"),e=a.length,c=null,d=null,i=0;i<e;++i){d=a[i].split("=");d[0]=decodeURIComponent(d[0]);d[1]=decodeURIComponent(d[1]);for(var f=d[0],c=[],g=-1;(g=f.indexOf("["))!==-1;)c.push(f.substr(g,f.indexOf("]")-g+1)),f=f.substr(f.indexOf("]")+1);if(c.length===0)b[d[0]]=d[1];else{g=d[0].substr(0,d[0].indexOf("["));typeof b[g]===
"undefined"&&(b[g]={});for(var f=b[g],t=c.length,p=0;p<t-1;++p)g=c[p].substr(1,c[p].length-2),typeof f[g]==="undefined"&&(f[g]={}),f=f[g];c=c[t-1];g=c.substr(1,c.length-2);f[g]=d[1]}}return b};return a};typeof a!=="undefined"?a=b():exports.createSerializer=b}return{postMessage:function(b,c,d,e){if(typeof window!=="undefined")b=a.serialize(b),typeof window.postMessage==="undefined"?e.location.href=d+"#"+ +new Date+Math.floor(Math.random()*1E3)+"&"+b:e.postMessage(b,c)},receiveMessage:function(b,c){if(typeof window!==
"undefined")if(window.postMessage)attachedCallback=function(d){if(d.origin.toLowerCase()!==c.toLowerCase())return!1;b(a.deserialize(d.data))},window.addEventListener?window.addEventListener("message",attachedCallback,!1):window.attachEvent("onmessage",attachedCallback);else{var d=window.location.hash;setInterval(function(){var c=window.location.hash,e=/^#?\d+&/;if(c!==d&&e.test(c))d=c,window.location.hash="",b(a.deserialize(c.replace(e,"")))},100)}}}};typeof a!=="undefined"?a=e():exports.createXD=
e}var d=null,o=[],l=0,h={},f={},k=!1,m,q,j={apiURL:"https://api.stripe.com",onMessage:function(a){var b=a.id,c=null,c=a.response===null||a.response===""?{error:{message:"There was an error processing your card"}}:JSON.parse(a.response);h[b](parseInt(a.status),c);window.clearTimeout(f[b]);delete h[b];delete f[b]}},n=!1,s=function(){b();n||(a.receiveMessage(j.onMessage,j.apiURL),n=!0)};j.init=function(){if(!m||!document.getElementById(m))typeof document!=="undefined"&&document&&document.body?s():typeof window!==
"undefined"&&window&&!k&&(window.addEventListener?window.addEventListener("load",s,!1):window.attachEvent&&window.attachEvent("onload",s)),k=!0};j.callAPI=function(a,b,d,e,f){if(a!=="POST"&&a!=="GET"&&a!=="DELETE")throw"You can only call the API with POST, GET or DELETE";j.init();var h=(l++).toString();o.push({message:{id:h,method:a,url:"/v1/"+b,params:d,key:e},callback:f});c()};return j};typeof l!=="undefined"?l=v():exports.createTransport=v;c.transport=l;c.validateCardNumber=function(a){var a=a.replace(/\s+|-/g,
""),b;if(b=a.length>=10)if(b=a.length<=16)if(a.match(/^[0-9]+$/)===null)b=!1;else{var a=a.split("").reverse().join(""),c=0,d;for(b=0;b<a.length;++b)d=parseInt(a.charAt(b),10),b%2!=0&&(d*=2),c+=d<10?d:d-9;b=c!=0&&c%10==0}return b};c.cardType=function(a){if(!d){d={};for(var b=40;b<=49;++b)d[b]="Visa";for(b=50;b<=59;++b)d[b]="MasterCard";d[34]=d[37]="American Express";d[60]=d[62]=d[64]=d[65]="Discover";d[35]="JCB";d[30]=d[36]=d[38]=d[39]="Diners Club"}a=d[a.substr(0,2)];return typeof a==="undefined"?
"Unknown":a};c.validateCVC=function(a){a=k(a);return a.match(/^[0-9]+$/)!==null&&a.length>=3&&a.length<=4};c.validateExpiry=function(a,b){var a=k(a),b=k(b),c=new Date;return a.match(/^[0-9]+$/)!==null&&b.match(/^[0-9]+$/)!==null&&b>c.getFullYear()||b==c.getFullYear()&&a>=c.getMonth()+1};c.createToken=function(a,b,d){typeof b==="function"&&(d=b,b=null);n();var e={expMonth:"exp_month",expYear:"exp_year",addressLine1:"address_line_1",addressLine2:"address_line_2",addressZip:"address_zip",addressState:"address_state",
addressCountry:"address_country"};for(convertibleParam in e)e.hasOwnProperty(convertibleParam)&&a.hasOwnProperty(convertibleParam)&&(a[e[convertibleParam]]=a[convertibleParam],delete a[convertibleParam]);params={card:a};b!==null&&(params.amount=b);c.transport.callAPI("POST","tokens",params,c.publishableKey,d)};c.getToken=function(a,b){n();c.transport.callAPI("GET","tokens/"+a,{},c.publishableKey,b)};c.setPublishableKey=function(a){c.publishableKey=a};l.init()})(typeof exports!=="undefined"&&exports!==
null?exports:window.Stripe={});

21
externals/stripe-php/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2010 Stripe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

20
externals/stripe-php/README.rdoc vendored Normal file
View file

@ -0,0 +1,20 @@
= Installation
Obtain the latest version of the Stripe PHP bindings with:
git clone https://github.com/stripe/stripe-php
To get started, add the following to your PHP script:
require_once("/path/to/stripe-php/lib/Stripe.php");
Simple usage looks like:
Stripe::setApiKey('d8e8fca2dc0f896fd7cb4cb0031ba249');
$myCard = array('number' => '4242424242424242', 'exp_month' => 5, 'exp_year' => 2015);
$charge = Stripe_Charge::create(array('card' => $myCard, 'amount' => 2000, 'currency' => 'usd'));
echo $charge;
= Documentation
Please see https://stripe.com/api for up-to-date documentation.

1
externals/stripe-php/VERSION vendored Normal file
View file

@ -0,0 +1 @@
1.6.3

66
externals/stripe-php/lib/Stripe.php vendored Normal file
View file

@ -0,0 +1,66 @@
<?php
// Tested on PHP 5.2, 5.3
// This snippet (and some of the curl code) due to the Facebook SDK.
if (!function_exists('curl_init')) {
throw new Exception('Stripe needs the CURL PHP extension.');
}
if (!function_exists('json_decode')) {
throw new Exception('Stripe needs the JSON PHP extension.');
}
abstract class Stripe
{
public static $apiKey;
public static $apiBase = 'https://api.stripe.com/v1';
public static $verifySslCerts = true;
const VERSION = '1.6.3';
public static function getApiKey()
{
return self::$apiKey;
}
public static function setApiKey($apiKey)
{
self::$apiKey = $apiKey;
}
public static function getVerifySslCerts() {
return self::$verifySslCerts;
}
public static function setVerifySslCerts($verify) {
self::$verifySslCerts = $verify;
}
}
// Utilities
require(dirname(__FILE__) . '/Stripe/Util.php');
require(dirname(__FILE__) . '/Stripe/Util/Set.php');
// Errors
require(dirname(__FILE__) . '/Stripe/Error.php');
require(dirname(__FILE__) . '/Stripe/ApiError.php');
require(dirname(__FILE__) . '/Stripe/ApiConnectionError.php');
require(dirname(__FILE__) . '/Stripe/AuthenticationError.php');
require(dirname(__FILE__) . '/Stripe/CardError.php');
require(dirname(__FILE__) . '/Stripe/InvalidRequestError.php');
// Plumbing
require(dirname(__FILE__) . '/Stripe/Object.php');
require(dirname(__FILE__) . '/Stripe/ApiRequestor.php');
require(dirname(__FILE__) . '/Stripe/ApiResource.php');
// Stripe API Resources
require(dirname(__FILE__) . '/Stripe/Charge.php');
require(dirname(__FILE__) . '/Stripe/Customer.php');
require(dirname(__FILE__) . '/Stripe/Invoice.php');
require(dirname(__FILE__) . '/Stripe/InvoiceItem.php');
require(dirname(__FILE__) . '/Stripe/Plan.php');
require(dirname(__FILE__) . '/Stripe/Token.php');
require(dirname(__FILE__) . '/Stripe/Coupon.php');
require(dirname(__FILE__) . '/Stripe/Event.php');

View file

@ -0,0 +1,5 @@
<?php
class Stripe_ApiConnectionError extends Stripe_Error
{
}

View file

@ -0,0 +1,5 @@
<?php
class Stripe_ApiError extends Stripe_Error
{
}

View file

@ -0,0 +1,197 @@
<?php
class Stripe_ApiRequestor
{
public $apiKey;
public function __construct($apiKey=null)
{
$this->_apiKey = $apiKey;
}
public static function apiUrl($url='')
{
$apiBase = Stripe::$apiBase;
return "$apiBase$url";
}
public static function utf8($value)
{
if (is_string($value))
return utf8_encode($value);
else
return $value;
}
private static function _encodeObjects($d)
{
if ($d instanceof Stripe_ApiRequestor) {
return $d->id;
} else if ($d === true) {
return 'true';
} else if ($d === false) {
return 'false';
} else if (is_array($d)) {
$res = array();
foreach ($d as $k => $v)
$res[$k] = self::_encodeObjects($v);
return $res;
} else {
return $d;
}
}
public static function encode($d)
{
return http_build_query($d, null, '&');
}
public function request($meth, $url, $params=null)
{
if (!$params)
$params = array();
list($rbody, $rcode, $myApiKey) = $this->_requestRaw($meth, $url, $params);
$resp = $this->_interpretResponse($rbody, $rcode);
return array($resp, $myApiKey);
}
public function handleApiError($rbody, $rcode, $resp)
{
if (!is_array($resp) || !isset($resp['error']))
throw new Stripe_ApiError("Invalid response object from API: $rbody (HTTP response code was $rcode)", $rcode, $rbody, $resp);
$error = $resp['error'];
switch ($rcode) {
case 400:
case 404:
throw new Stripe_InvalidRequestError(isset($error['message']) ? $error['message'] : null,
isset($error['param']) ? $error['param'] : null,
$rcode, $rbody, $resp);
case 401:
throw new Stripe_AuthenticationError(isset($error['message']) ? $error['message'] : null, $rcode, $rbody, $resp);
case 402:
throw new Stripe_CardError(isset($error['message']) ? $error['message'] : null,
isset($error['param']) ? $error['param'] : null,
isset($error['code']) ? $error['code'] : null,
$rcode, $rbody, $resp);
default:
throw new Stripe_ApiError(isset($error['message']) ? $error['message'] : null, $rcode, $rbody, $resp);
}
}
private function _requestRaw($meth, $url, $params)
{
$myApiKey = $this->_apiKey;
if (!$myApiKey)
$myApiKey = Stripe::$apiKey;
if (!$myApiKey)
throw new Stripe_AuthenticationError('No API key provided. (HINT: set your API key using "Stripe::setApiKey(<API-KEY>)". You can generate API keys from the Stripe web interface. See https://stripe.com/api for details, or email support@stripe.com if you have any questions.');
$absUrl = $this->apiUrl($url);
$params = self::_encodeObjects($params);
$langVersion = phpversion();
$uname = php_uname();
$ua = array('bindings_version' => Stripe::VERSION,
'lang' => 'php',
'lang_version' => $langVersion,
'publisher' => 'stripe',
'uname' => $uname);
$headers = array('X-Stripe-Client-User-Agent: ' . json_encode($ua),
'User-Agent: Stripe/v1 PhpBindings/' . Stripe::VERSION);
list($rbody, $rcode) = $this->_curlRequest($meth, $absUrl, $headers, $params, $myApiKey);
return array($rbody, $rcode, $myApiKey);
}
private function _interpretResponse($rbody, $rcode)
{
try {
$resp = json_decode($rbody, true);
} catch (Exception $e) {
throw new Stripe_ApiError("Invalid response body from API: $rbody (HTTP response code was $rcode)", $rcode, $rbody);
}
if ($rcode < 200 || $rcode >= 300) {
$this->handleApiError($rbody, $rcode, $resp);
}
return $resp;
}
private function _curlRequest($meth, $absUrl, $headers, $params, $myApiKey)
{
$curl = curl_init();
$meth = strtolower($meth);
$opts = array();
if ($meth == 'get') {
$opts[CURLOPT_HTTPGET] = 1;
if (count($params) > 0) {
$encoded = self::encode($params);
$absUrl = "$absUrl?$encoded";
}
} else if ($meth == 'post') {
$opts[CURLOPT_POST] = 1;
$opts[CURLOPT_POSTFIELDS] = self::encode($params);
} else if ($meth == 'delete') {
$opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
if (count($params) > 0) {
$encoded = self::encode($params);
$absUrl = "$absUrl?$encoded";
}
} else {
throw new Stripe_ApiError("Unrecognized method $meth");
}
$absUrl = self::utf8($absUrl);
$opts[CURLOPT_URL] = $absUrl;
$opts[CURLOPT_RETURNTRANSFER] = true;
$opts[CURLOPT_CONNECTTIMEOUT] = 30;
$opts[CURLOPT_TIMEOUT] = 80;
$opts[CURLOPT_RETURNTRANSFER] = true;
$opts[CURLOPT_HTTPHEADER] = $headers;
$opts[CURLOPT_USERPWD] = $myApiKey . ':';
if (!Stripe::$verifySslCerts)
$opts[CURLOPT_SSL_VERIFYPEER] = false;
curl_setopt_array($curl, $opts);
$rbody = curl_exec($curl);
$errno = curl_errno($curl);
if ($errno == CURLE_SSL_CACERT || $errno == CURLE_SSL_PEER_CERTIFICATE) {
array_push($headers, 'X-Stripe-Client-Info: {"ca":"using Stripe-supplied CA bundle"}');
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_CAINFO,
dirname(__FILE__) . '/../data/ca-certificates.crt');
$rbody = curl_exec($curl);
}
if ($rbody === false) {
$errno = curl_errno($curl);
$message = curl_error($curl);
curl_close($curl);
$this->handleCurlError($errno, $message);
}
$rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
return array($rbody, $rcode);
}
public function handleCurlError($errno, $message)
{
$apiBase = Stripe::$apiBase;
switch ($errno) {
case CURLE_COULDNT_CONNECT:
case CURLE_COULDNT_RESOLVE_HOST:
case CURLE_OPERATION_TIMEOUTED:
$msg = "Could not connect to Stripe ($apiBase). Please check your internet connection and try again. If this problem persists, you should check Stripe's service status at https://twitter.com/stripestatus, or let us know at support@stripe.com.";
break;
case CURLE_SSL_CACERT:
case CURLE_SSL_PEER_CERTIFICATE:
$msg = "Could not verify Stripe's SSL certificate. Please make sure that your network is not intercepting certificates. (Try going to $apiBase in your browser.) If this problem persists, let us know at support@stripe.com.";
break;
default:
$msg = "Unexpected error communicating with Stripe. If this problem persists, let us know at support@stripe.com.";
}
$msg .= "\n\n(Network error: $message)";
throw new Stripe_ApiConnectionError($msg);
}
}

View file

@ -0,0 +1,97 @@
<?php
abstract class Stripe_ApiResource extends Stripe_Object
{
protected static function _scopedRetrieve($class, $id, $apiKey=null)
{
$instance = new $class($id, $apiKey);
$instance->refresh();
return $instance;
}
public function refresh()
{
$requestor = new Stripe_ApiRequestor($this->_apiKey);
$url = $this->instanceUrl();
list($response, $apiKey) = $requestor->request('get', $url);
$this->refreshFrom($response, $apiKey);
return $this;
}
public static function classUrl($class)
{
// Useful for namespaces: Foo\Stripe_Charge
if ($postfix = strrchr($class, '\\'))
$class = substr($postfix, 1);
if (substr($class, 0, strlen('Stripe')) == 'Stripe')
$class = substr($class, strlen('Stripe'));
$class = str_replace('_', '', $class);
$name = urlencode($class);
$name = strtolower($name);
return "/${name}s";
}
public function instanceUrl()
{
$id = $this['id'];
$class = get_class($this);
if (!$id) {
throw new Stripe_InvalidRequestError("Could not determine which URL to request: $class instance has invalid ID: $id");
}
$id = Stripe_ApiRequestor::utf8($id);
$base = self::classUrl($class);
$extn = urlencode($id);
return "$base/$extn";
}
private static function _validateCall($method, $params=null, $apiKey=null)
{
if ($params && !is_array($params))
throw new Stripe_Error("You must pass an array as the first argument to Stripe API method calls. (HINT: an example call to create a charge would be: \"StripeCharge::create(array('amount' => 100, 'currency' => 'usd', 'card' => array('number' => 4242424242424242, 'exp_month' => 5, 'exp_year' => 2015)))\")");
if ($apiKey && !is_string($apiKey))
throw new Stripe_Error('The second argument to Stripe API method calls is an optional per-request apiKey, which must be a string. (HINT: you can set a global apiKey by "Stripe::setApiKey(<apiKey>)")');
}
protected static function _scopedAll($class, $params=null, $apiKey=null)
{
self::_validateCall('all', $params, $apiKey);
$requestor = new Stripe_ApiRequestor($apiKey);
$url = self::classUrl($class);
list($response, $apiKey) = $requestor->request('get', $url, $params);
return Stripe_Util::convertToStripeObject($response, $apiKey);
}
protected static function _scopedCreate($class, $params=null, $apiKey=null)
{
self::_validateCall('create', $params, $apiKey);
$requestor = new Stripe_ApiRequestor($apiKey);
$url = self::classUrl($class);
list($response, $apiKey) = $requestor->request('post', $url, $params);
return Stripe_Util::convertToStripeObject($response, $apiKey);
}
protected function _scopedSave($class)
{
self::_validateCall('save');
if ($this->_unsavedValues) {
$requestor = new Stripe_ApiRequestor($this->_apiKey);
$params = array();
foreach ($this->_unsavedValues->toArray() as $k)
$params[$k] = $this->$k;
$url = $this->instanceUrl();
list($response, $apiKey) = $requestor->request('post', $url, $params);
$this->refreshFrom($response, $apiKey);
}
return $this;
}
protected function _scopedDelete($class, $params=null)
{
self::_validateCall('delete');
$requestor = new Stripe_ApiRequestor($this->_apiKey);
$url = $this->instanceUrl();
list($response, $apiKey) = $requestor->request('delete', $url, $params);
$this->refreshFrom($response, $apiKey);
return $this;
}
}

View file

@ -0,0 +1,5 @@
<?php
class Stripe_AuthenticationError extends Stripe_Error
{
}

View file

@ -0,0 +1,11 @@
<?php
class Stripe_CardError extends Stripe_Error
{
public function __construct($message, $param, $code, $http_status=null, $http_body=null, $json_body=null)
{
parent::__construct($message, $http_status, $http_body, $json_body);
$this->param = $param;
$this->code = $code;
}
}

View file

@ -0,0 +1,46 @@
<?php
class Stripe_Charge extends Stripe_ApiResource
{
public static function constructFrom($values, $apiKey=null)
{
$class = get_class();
return self::scopedConstructFrom($class, $values, $apiKey);
}
public static function retrieve($id, $apiKey=null)
{
$class = get_class();
return self::_scopedRetrieve($class, $id, $apiKey);
}
public static function all($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedAll($class, $params, $apiKey);
}
public static function create($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedCreate($class, $params, $apiKey);
}
public function refund($params=null)
{
$requestor = new Stripe_ApiRequestor($this->_apiKey);
$url = $this->instanceUrl() . '/refund';
list($response, $apiKey) = $requestor->request('post', $url, $params);
$this->refreshFrom($response, $apiKey);
return $this;
}
public function capture($params=null)
{
$requestor = new Stripe_ApiRequestor($this->_apiKey);
$url = $this->instanceUrl() . '/capture';
list($response, $apiKey) = $requestor->request('post', $url, $params);
$this->refreshFrom($response, $apiKey);
return $this;
}
}

View file

@ -0,0 +1,34 @@
<?php
class Stripe_Coupon extends Stripe_ApiResource
{
public static function constructFrom($values, $apiKey=null)
{
$class = get_class();
return self::scopedConstructFrom($class, $values, $apiKey);
}
public static function retrieve($id, $apiKey=null)
{
$class = get_class();
return self::_scopedRetrieve($class, $id, $apiKey);
}
public static function create($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedCreate($class, $params, $apiKey);
}
public function delete($params=null)
{
$class = get_class();
return self::_scopedDelete($class, $params);
}
public static function all($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedAll($class, $params, $apiKey);
}
}

View file

@ -0,0 +1,94 @@
<?php
class Stripe_Customer extends Stripe_ApiResource
{
public static function constructFrom($values, $apiKey=null)
{
$class = get_class();
return self::scopedConstructFrom($class, $values, $apiKey);
}
public static function retrieve($id, $apiKey=null)
{
$class = get_class();
return self::_scopedRetrieve($class, $id, $apiKey);
}
public static function all($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedAll($class, $params, $apiKey);
}
public static function create($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedCreate($class, $params, $apiKey);
}
public function save()
{
$class = get_class();
return self::_scopedSave($class);
}
public function delete($params=null)
{
$class = get_class();
return self::_scopedDelete($class, $params);
}
public function addInvoiceItem($params=null)
{
if (!$params)
$params = array();
$params['customer'] = $this->id;
$ii = Stripe_InvoiceItem::create($params, $this->_apiKey);
return $ii;
}
public function invoices($params=null)
{
if (!$params)
$params = array();
$params['customer'] = $this->id;
$invoices = Stripe_Invoice::all($params, $this->_apiKey);
return $invoices;
}
public function invoiceItems($params=null)
{
if (!$params)
$params = array();
$params['customer'] = $this->id;
$iis = Stripe_InvoiceItem::all($params, $this->_apiKey);
return $iis;
}
public function charges($params=null)
{
if (!$params)
$params = array();
$params['customer'] = $this->id;
$charges = Stripe_Charge::all($params, $this->_apiKey);
return $charges;
}
public function updateSubscription($params=null)
{
$requestor = new Stripe_ApiRequestor($this->_apiKey);
$url = $this->instanceUrl() . '/subscription';
list($response, $apiKey) = $requestor->request('post', $url, $params);
$this->refreshFrom(array('subscription' => $response), $apiKey, true);
return $this->subscription;
}
public function cancelSubscription($params=null)
{
$requestor = new Stripe_ApiRequestor($this->_apiKey);
$url = $this->instanceUrl() . '/subscription';
list($response, $apiKey) = $requestor->request('delete', $url, $params);
$this->refreshFrom(array('subscription' => $response), $apiKey, true);
return $this->subscription;
}
}

View file

@ -0,0 +1,27 @@
<?php
class Stripe_Error extends Exception
{
public function __construct($message=null, $http_status=null, $http_body=null, $json_body=null)
{
parent::__construct($message);
$this->http_status = $http_status;
$this->http_body = $http_body;
$this->json_body = $json_body;
}
public function getHttpStatus()
{
return $this->http_status;
}
public function getHttpBody()
{
return $this->http_body;
}
public function getJsonBody()
{
return $this->json_body;
}
}

View file

@ -0,0 +1,22 @@
<?php
class Stripe_Event extends Stripe_ApiResource
{
public static function constructFrom($values, $apiKey=null)
{
$class = get_class();
return self::scopedConstructFrom($class, $values, $apiKey);
}
public static function retrieve($id, $apiKey=null)
{
$class = get_class();
return self::_scopedRetrieve($class, $id, $apiKey);
}
public static function all($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedAll($class, $params, $apiKey);
}
}

View file

@ -0,0 +1,10 @@
<?php
class Stripe_InvalidRequestError extends Stripe_Error
{
public function __construct($message, $param, $http_status=null, $http_body=null, $json_body=null)
{
parent::__construct($message, $http_status, $http_body, $json_body);
$this->param = $param;
}
}

View file

@ -0,0 +1,30 @@
<?php
class Stripe_Invoice extends Stripe_ApiResource
{
public static function constructFrom($values, $apiKey=null)
{
$class = get_class();
return self::scopedConstructFrom($class, $values, $apiKey);
}
public static function retrieve($id, $apiKey=null)
{
$class = get_class();
return self::_scopedRetrieve($class, $id, $apiKey);
}
public static function all($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedAll($class, $params, $apiKey);
}
public static function upcoming($params=null, $apiKey=null)
{
$requestor = new Stripe_ApiRequestor($apiKey);
$url = self::classUrl(get_class()) . '/upcoming';
list($response, $apiKey) = $requestor->request('get', $url, $params);
return Stripe_Util::convertToStripeObject($response, $apiKey);
}
}

View file

@ -0,0 +1,40 @@
<?php
class Stripe_InvoiceItem extends Stripe_ApiResource
{
public static function constructFrom($values, $apiKey=null)
{
$class = get_class();
return self::scopedConstructFrom($class, $values, $apiKey);
}
public static function retrieve($id, $apiKey=null)
{
$class = get_class();
return self::_scopedRetrieve($class, $id, $apiKey);
}
public static function all($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedAll($class, $params, $apiKey);
}
public static function create($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedCreate($class, $params, $apiKey);
}
public function save()
{
$class = get_class();
return self::_scopedSave($class);
}
public function delete($params=null)
{
$class = get_class();
return self::_scopedDelete($class, $params);
}
}

View file

@ -0,0 +1,142 @@
<?php
class Stripe_Object implements ArrayAccess
{
public static $_permanentAttributes;
public static function init()
{
self::$_permanentAttributes = new Stripe_Util_Set(array('_apiKey'));
}
protected $_apiKey;
protected $_values;
protected $_unsavedValues;
protected $_transientValues;
public function __construct($id=null, $apiKey=null)
{
$this->_apiKey = $apiKey;
$this->_values = array();
$this->_unsavedValues = new Stripe_Util_Set();
$this->_transientValues = new Stripe_Util_Set();
if ($id)
$this->id = $id;
}
// Standard accessor magic methods
public function __set($k, $v)
{
// TODO: may want to clear from $_transientValues. (Won't be user-visible.)
$this->_values[$k] = $v;
if (!self::$_permanentAttributes->includes($k))
$this->_unsavedValues->add($k);
}
public function __isset($k)
{
return isset($this->_values[$k]);
}
public function __unset($k)
{
unset($this->_values[$k]);
$this->_transientValues->add($k);
$this->_unsavedValues->discard($k);
}
public function __get($k)
{
if (isset($this->_values[$k])) {
return $this->_values[$k];
} else if ($this->_transientValues->includes($k)) {
$class = get_class($this);
$attrs = join(', ', array_keys($this->_values));
error_log("Stripe Notice: Undefined property of $class instance: $k. HINT: The $k attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by Stripe's API, probably as a result of a save(). The attributes currently available on this object are: $attrs");
return null;
} else {
$class = get_class($this);
error_log("Stripe Notice: Undefined property of $class instance: $k");
return null;
}
}
// ArrayAccess methods
public function offsetSet($k, $v)
{
$this->$k = $v;
}
public function offsetExists($k)
{
return isset($this->$k);
}
public function offsetUnset($k)
{
unset($this->$k);
}
public function offsetGet($k)
{
return isset($this->_values[$k]) ? $this->_values[$k] : null;
}
// This unfortunately needs to be public to be used in Util.php
public static function scopedConstructFrom($class, $values, $apiKey=null)
{
$obj = new $class(isset($values['id']) ? $values['id'] : null, $apiKey);
$obj->refreshFrom($values, $apiKey);
return $obj;
}
public static function constructFrom($values, $apiKey=null)
{
$class = get_class();
return self::scopedConstructFrom($class, $values, $apiKey);
}
public function refreshFrom($values, $apiKey, $partial=false)
{
$this->_apiKey = $apiKey;
// Wipe old state before setting new. This is useful for e.g. updating a
// customer, where there is no persistent card parameter. Mark those values
// which don't persist as transient
if ($partial)
$removed = new Stripe_Util_Set();
else
$removed = array_diff(array_keys($this->_values), array_keys($values));
foreach ($removed as $k) {
if (self::$_permanentAttributes->includes($k))
continue;
unset($this->$k);
}
foreach ($values as $k => $v) {
if (self::$_permanentAttributes->includes($k))
continue;
$this->_values[$k] = Stripe_Util::convertToStripeObject($v, $apiKey);
$this->_transientValues->discard($k);
$this->_unsavedValues->discard($k);
}
}
public function __toJSON()
{
if (defined('JSON_PRETTY_PRINT'))
return json_encode($this->__toArray(true), JSON_PRETTY_PRINT);
else
return json_encode($this->__toArray(true));
}
public function __toString()
{
return $this->__toJSON();
}
public function __toArray($recursive=false)
{
if ($recursive)
return Stripe_Util::convertStripeObjectToArray($this->_values);
else
return $this->_values;
}
}
Stripe_Object::init();

View file

@ -0,0 +1,40 @@
<?php
class Stripe_Plan extends Stripe_ApiResource
{
public static function constructFrom($values, $apiKey=null)
{
$class = get_class();
return self::scopedConstructFrom($class, $values, $apiKey);
}
public static function retrieve($id, $apiKey=null)
{
$class = get_class();
return self::_scopedRetrieve($class, $id, $apiKey);
}
public static function create($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedCreate($class, $params, $apiKey);
}
public function delete($params=null)
{
$class = get_class();
return self::_scopedDelete($class, $params);
}
public function save()
{
$class = get_class();
return self::_scopedSave($class);
}
public static function all($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedAll($class, $params, $apiKey);
}
}

View file

@ -0,0 +1,22 @@
<?php
class Stripe_Token extends Stripe_ApiResource
{
public static function constructFrom($values, $apiKey=null)
{
$class = get_class();
return self::scopedConstructFrom($class, $values, $apiKey);
}
public static function retrieve($id, $apiKey=null)
{
$class = get_class();
return self::_scopedRetrieve($class, $id, $apiKey);
}
public static function create($params=null, $apiKey=null)
{
$class = get_class();
return self::_scopedCreate($class, $params, $apiKey);
}
}

View file

@ -0,0 +1,59 @@
<?php
abstract class Stripe_Util
{
public static function isList($array)
{
if (!is_array($array))
return false;
// TODO: this isn't actually correct in general, but it's correct given Stripe's responses
foreach (array_keys($array) as $k) {
if (!is_numeric($k))
return false;
}
return true;
}
public static function convertStripeObjectToArray($values)
{
$results = array();
foreach ($values as $k => $v) {
// FIXME: this is an encapsulation violation
if (Stripe_Object::$_permanentAttributes->includes($k)) {
continue;
}
if ($v instanceof Stripe_Object) {
$results[$k] = $v->__toArray(true);
}
else if (is_array($v)) {
$results[$k] = self::convertStripeObjectToArray($v);
}
else {
$results[$k] = $v;
}
}
return $results;
}
public static function convertToStripeObject($resp, $apiKey)
{
$types = array('charge' => 'Stripe_Charge',
'customer' => 'Stripe_Customer',
'invoice' => 'Stripe_Invoice',
'invoiceitem' => 'Stripe_InvoiceItem', 'event' => 'Stripe_Event');
if (self::isList($resp)) {
$mapped = array();
foreach ($resp as $i)
array_push($mapped, self::convertToStripeObject($i, $apiKey));
return $mapped;
} else if (is_array($resp)) {
if (isset($resp['object']) && is_string($resp['object']) && isset($types[$resp['object']]))
$class = $types[$resp['object']];
else
$class = 'Stripe_Object';
return Stripe_Object::scopedConstructFrom($class, $resp, $apiKey);
} else {
return $resp;
}
}
}

View file

@ -0,0 +1,34 @@
<?php
class Stripe_Util_Set
{
private $_elts;
public function __construct($members=array())
{
$this->_elts = array();
foreach ($members as $item)
$this->_elts[$item] = true;
}
public function includes($elt)
{
return isset($this->_elts[$elt]);
}
public function add($elt)
{
$this->_elts[$elt] = true;
}
public function discard($elt)
{
unset($this->_elts[$elt]);
}
// TODO: make Set support foreach
public function toArray()
{
return array_keys($this->_elts);
}
}

File diff suppressed because it is too large Load diff

View file

@ -993,6 +993,19 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/repository/repository-crossreference.js',
),
'javelin-behavior-stripe-payment-form' =>
array(
'uri' => '/res/b77a4b16/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-json',
3 => 'stripe-core',
),
'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
),
'javelin-behavior-view-placeholder' =>
array(
'uri' => '/res/5b89bdf5/rsrc/js/javelin/ext/view/ViewPlaceholder.js',
@ -2022,6 +2035,24 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/raphael/g.raphael.line.js',
),
'stripe-core' =>
array(
'uri' => '/res/3b0f0ad4/rsrc/js/stripe/stripe_core.js',
'type' => 'js',
'requires' =>
array(
),
'disk' => '/rsrc/js/stripe/stripe_core.js',
),
'stripe-payment-form-css' =>
array(
'uri' => '/res/e2358ded/rsrc/css/application/phortune/stripe-payment-form.css',
'type' => 'css',
'requires' =>
array(
),
'disk' => '/rsrc/css/application/phortune/stripe-payment-form.css',
),
'syntax-highlighting-css' =>
array(
'uri' => '/res/5669beb6/rsrc/css/core/syntax.css',

View file

@ -914,6 +914,10 @@ phutil_register_library_map(array(
'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/symbol',
'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/toplevel',
'PhabricatorXHProfProfileView' => 'applications/xhprof/view/base',
'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/monthyearexpiry',
'PhortuneStripeBaseController' => 'applications/phortune/stripe/controller/base',
'PhortuneStripePaymentFormView' => 'applications/phortune/stripe/view/paymentform',
'PhortuneStripeTestPaymentFormController' => 'applications/phortune/stripe/controller/testpaymentform',
'PhrictionActionConstants' => 'applications/phriction/constants/action',
'PhrictionChangeType' => 'applications/phriction/constants/changetype',
'PhrictionConstants' => 'applications/phriction/constants/base',
@ -1712,6 +1716,10 @@ phutil_register_library_map(array(
'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView',
'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView',
'PhabricatorXHProfProfileView' => 'AphrontView',
'PhortuneMonthYearExpiryControl' => 'AphrontFormControl',
'PhortuneStripeBaseController' => 'PhabricatorController',
'PhortuneStripePaymentFormView' => 'AphrontView',
'PhortuneStripeTestPaymentFormController' => 'PhortuneStripeBaseController',
'PhrictionActionConstants' => 'PhrictionConstants',
'PhrictionChangeType' => 'PhrictionConstants',
'PhrictionContent' => 'PhrictionDAO',

View file

@ -412,6 +412,12 @@ class AphrontDefaultApplicationConfiguration
'edit/(?P<phid>[^/]+)/' => 'PhabricatorFlagEditController',
'delete/(?P<id>\d+)/' => 'PhabricatorFlagDeleteController',
),
'/phortune/' => array(
'stripe/' => array(
'testpaymentform/' => 'PhortuneStripeTestPaymentFormController',
),
),
);
}

View file

@ -0,0 +1,116 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhortuneMonthYearExpiryControl extends AphrontFormControl {
private $user;
private $monthValue;
private $yearValue;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
private function getUser() {
return $this->user;
}
public function setMonthInputValue($value) {
$this->monthValue = $value;
return $this;
}
private function getMonthInputValue() {
return $this->monthValue;
}
private function getCurrentMonth() {
return phabricator_format_local_time(
time(),
$this->getUser(),
'm');
}
public function setYearInputValue($value) {
$this->yearValue = $value;
return $this;
}
private function getYearInputValue() {
return $this->yearValue;
}
private function getCurrentYear() {
return phabricator_format_local_time(
time(),
$this->getUser(),
'Y');
}
protected function getCustomControlClass() {
return 'aphront-form-control-text';
}
protected function renderInput() {
if (!$this->getUser()) {
throw new Exception('You must setUser() before render()!');
}
// represent months like a credit card does
$months = array(
'01' => '01',
'02' => '02',
'03' => '03',
'04' => '04',
'05' => '05',
'06' => '06',
'07' => '07',
'08' => '08',
'09' => '09',
'10' => '10',
'11' => '11',
'12' => '12',
);
$current_year = $this->getCurrentYear();
$years = range($current_year, $current_year + 20);
$years = array_combine($years, $years);
if ($this->getMonthInputValue()) {
$selected_month = $this->getMonthInputValue();
} else {
$selected_month = $this->getCurrentMonth();
}
$months_sel = AphrontFormSelectControl::renderSelectTag(
$selected_month,
$months,
array(
'sigil' => 'month-input',
));
$years_sel = AphrontFormSelectControl::renderSelectTag(
$this->getYearInputValue(),
$years,
array(
'sigil' => 'year-input',
));
return self::renderSingleView(
array(
$months_sel,
$years_sel
)
);
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'view/form/control/base');
phutil_require_module('phabricator', 'view/form/control/select');
phutil_require_module('phabricator', 'view/utils');
phutil_require_source('PhortuneMonthYearExpiryControl.php');

View file

@ -0,0 +1,33 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PhortuneStripeBaseController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Phortune - Stripe');
$page->setBaseURI('/phortune/stripe/');
$page->setTitle(idx($data, 'title'));
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/response/webpage');
phutil_require_module('phabricator', 'applications/base/controller/base');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhortuneStripeBaseController.php');

View file

@ -0,0 +1,166 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhortuneStripeTestPaymentFormController
extends PhortuneStripeBaseController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$title = 'Test Payment Form';
$error_view = null;
$card_number_error = null;
$card_cvc_error = null;
$card_expiration_error = null;
$stripe_key = $request->getStr('stripeKey');
if (!$stripe_key) {
$error_view = id(new AphrontErrorView())
->setTitle('Missing stripeKey parameter in URI');
}
if (!$error_view && $request->isFormPost()) {
$card_errors = $request->getStr('cardErrors');
$stripe_token = $request->getStr('stripeToken');
if ($card_errors) {
$raw_errors = json_decode($card_errors);
list($card_number_error,
$card_cvc_error,
$card_expiration_error,
$messages) = $this->parseRawErrors($raw_errors);
$error_view = id(new AphrontErrorView())
->setTitle('There were errors processing your card.')
->setErrors($messages);
} else if (!$stripe_token) {
// this shouldn't happen, so show the user a very generic error
// message and log that this error occurred...!
$error_view = id(new AphrontErrorView())
->setTitle('There was an unknown error processing your card.')
->setErrors(array('Please try again.'));
$error = 'payment form submitted but no stripe token and no errors';
$this->logStripeError($error);
} else {
// success -- do something with $stripe_token!!
}
} else if (!$error_view) {
$error_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle(
'If you are using a test stripe key, use 4242424242424242, '.
'any three digits for CVC, and any valid expiration date to '.
'test!');
}
$view = id(new AphrontPanelView())
->setWidth(AphrontPanelView::WIDTH_FORM)
->setHeader($title);
$form = id(new PhortuneStripePaymentFormView())
->setUser($user)
->setStripeKey($stripe_key)
->setCardNumberError($card_number_error)
->setCardCVCError($card_cvc_error)
->setCardExpirationError($card_expiration_error);
$view->appendChild($form);
return
$this->buildStandardPageResponse(
array(
$error_view,
$view,
),
array(
'title' => $title,
)
);
}
/**
* Stripe JS and calls to Stripe handle all errors with processing this
* form. This function takes the raw errors - in the form of an array
* where each elementt is $type => $message - and figures out what if
* any fields were invalid and pulls the messages into a flat object.
*
* See https://stripe.com/docs/api#errors for more information on possible
* errors.
*/
private function parseRawErrors($errors) {
$card_number_error = null;
$card_cvc_error = null;
$card_expiration_error = null;
$messages = array();
foreach ($errors as $index => $error) {
$type = key($error);
$msg = reset($error);
$messages[] = $msg;
switch ($type) {
case 'number':
case 'invalid_number':
case 'incorrect_number':
$card_number_error = true;
break;
case 'cvc':
case 'invalid_cvc':
case 'incorrect_cvc':
$card_cvc_error = true;
break;
case 'expiry':
case 'invalid_expiry_month':
case 'invalid_expiry_year':
$card_expiration_error = true;
break;
case 'card_declined':
case 'expired_card':
case 'duplicate_transaction':
case 'processing_error':
// these errors don't map well to field(s) being bad
break;
case 'invalid_amount':
case 'missing':
default:
// these errors only happen if we (not the user) messed up so log it
$error = sprintf(
'error_type: %s error_message: %s',
$type,
$msg
);
$this->logStripeError($error);
break;
}
}
// append a helpful "fix this" to the messages to be displayed to the user
if (count($messages) == 1) {
$messages[] = 'Please fix this error and try again.';
} else {
$messages[] = 'Please fix these errors and try again.';
}
return array(
$card_number_error,
$card_cvc_error,
$card_expiration_error,
$messages
);
}
private function logStripeError($message) {
phlog('STRIPE-ERROR '.$message);
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/phortune/stripe/controller/base');
phutil_require_module('phabricator', 'applications/phortune/stripe/view/paymentform');
phutil_require_module('phabricator', 'view/form/error');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phutil', 'error');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhortuneStripeTestPaymentFormController.php');

View file

@ -0,0 +1,158 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhortuneStripePaymentFormView extends AphrontView {
private $user;
private $stripeKey;
private $cardNumberError;
private $cardCVCError;
private $cardExpirationError;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
private function getUser() {
return $this->user;
}
public function setStripeKey($key) {
$this->stripeKey = $key;
return $this;
}
private function getStripeKey() {
return $this->stripeKey;
}
public function setCardNumberError($error) {
$this->cardNumberError = $error;
return $this;
}
private function getCardNumberError() {
return $this->cardNumberError;
}
public function setCardCVCError($error) {
$this->cardCVCError = $error;
return $this;
}
private function getCardCVCError() {
return $this->cardCVCError;
}
public function setCardExpirationError($error) {
$this->cardExpirationError = $error;
return $this;
}
private function getCardExpirationError() {
return $this->cardExpirationError;
}
public function render() {
$form_id = celerity_generate_unique_node_id();
require_celerity_resource('stripe-payment-form-css');
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips');
$form = id(new AphrontFormView())
->setID($form_id)
->setUser($this->getUser())
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('')
->setValue(
javelin_render_tag(
'div',
array(
'class' => 'credit-card-logos',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'We support Visa, Mastercard, American Express, '.
'Discover, JCB, and Diners Club.',
'size' => 440,
)
)
)
)
)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Card Number')
->setDisableAutocomplete(true)
->setSigil('number-input')
->setError($this->getCardNumberError())
)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('CVC')
->setDisableAutocomplete(true)
->setSigil('cvc-input')
->setError($this->getCardCVCError())
)
->appendChild(
id(new PhortuneMonthYearExpiryControl())
->setLabel('Expiration')
->setUser($this->getUser())
->setError($this->getCardExpirationError())
)
->appendChild(
javelin_render_tag(
'input',
array(
'hidden' => true,
'name' => 'stripeToken',
'sigil' => 'stripe-token-input',
)
)
)
->appendChild(
javelin_render_tag(
'input',
array(
'hidden' => true,
'name' => 'cardErrors',
'sigil' => 'card-errors-input'
)
)
)
->appendChild(
phutil_render_tag(
'input',
array(
'hidden' => true,
'name' => 'stripeKey',
'value' => $this->getStripeKey(),
)
)
)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Submit Payment')
);
Javelin::initBehavior(
'stripe-payment-form',
array(
'stripePublishKey' => $this->getStripeKey(),
'root' => $form_id,
)
);
return $form->render();
}
}

View file

@ -0,0 +1,23 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/phortune/control/monthyearexpiry');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'infrastructure/javelin/api');
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'view/base');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/markup');
phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'view/form/control/text');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhortuneStripePaymentFormView.php');

View file

@ -18,19 +18,39 @@
final class AphrontFormTextControl extends AphrontFormControl {
private $disableAutocomplete;
private $sigil;
public function setDisableAutocomplete($disable) {
$this->disableAutocomplete = $disable;
return $this;
}
private function getDisableAutocomplete() {
return $this->disableAutocomplete;
}
public function getSigil() {
return $this->sigil;
}
public function setSigil($sigil) {
$this->sigil = $sigil;
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-text';
}
protected function renderInput() {
return phutil_render_tag(
return javelin_render_tag(
'input',
array(
'type' => 'text',
'name' => $this->getName(),
'value' => $this->getValue(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'id' => $this->getID(),
'type' => 'text',
'name' => $this->getName(),
'value' => $this->getValue(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'autocomplete' => $this->getDisableAutocomplete() ? 'off' : null,
'id' => $this->getID(),
'sigil' => $this->getSigil(),
));
}

View file

@ -6,9 +6,8 @@
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'view/form/control/base');
phutil_require_module('phutil', 'markup');
phutil_require_source('AphrontFormTextControl.php');

View file

@ -0,0 +1,8 @@
/**
* @provides stripe-payment-form-css
*/
.credit-card-logos {
background: url(/rsrc/image/credit_cards.png) no-repeat 0px 2px;
height: 32px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -0,0 +1,123 @@
/**
* @provides javelin-behavior-stripe-payment-form
* @requires javelin-behavior
* javelin-dom
* javelin-json
* stripe-core
*/
JX.behavior('stripe-payment-form', function(config) {
Stripe.setPublishableKey(config.stripePublishKey);
var root = JX.$(config.root);
var cardErrors = JX.DOM.find(root, 'input', 'card-errors-input');
var stripeToken = JX.DOM.find(root, 'input', 'stripe-token-input');
var getCardData = function() {
return {
number : JX.DOM.find(root, 'input', 'number-input').value,
cvc : JX.DOM.find(root, 'input', 'cvc-input' ).value,
month : JX.DOM.find(root, 'select', 'month-input' ).value,
year : JX.DOM.find(root, 'select', 'year-input' ).value
};
}
var stripeErrorObject = function(type) {
var errorPre = 'Stripe (our payments provider) has detected your card ';
var errorPost = ' is invalid.';
var msg = '';
var result = {};
switch (type) {
case 'number':
msg = errorPre + 'number' + errorPost;
break;
case 'cvc':
msg = errorPre + 'CVC' + errorPost;
break;
case 'expiry':
msg = errorPre + 'expiration date' + errorPost;
break;
case 'stripe':
msg = 'Stripe (our payments provider) is experiencing issues. ' +
'Please try again.';
break;
case 'invalid_request':
default:
msg = 'Unknown error.';
// TODO - how best report bugs? would be good to get
// user feedback since this shouldn't happen!
break;
}
result[type] = msg;
return result;
}
var onsubmit = function(e) {
e.kill();
// validate the card data with Stripe client API and submit the form
// with any detected errors
var cardData = getCardData();
var errors = [];
if (!Stripe.validateCardNumber(cardData.number)) {
errors.push(stripeErrorObject('number'));
}
if (!Stripe.validateCVC(cardData.cvc)) {
errors.push(stripeErrorObject('cvc'));
}
if (!Stripe.validateExpiry(cardData.month,
cardData.year)) {
errors.push(stripeErrorObject('expiry'));
}
if (errors.length != 0) {
cardErrors.value = JX.JSON.stringify(errors);
root.submit();
return true;
}
// no errors detected so contact Stripe asynchronously
var submitData = {
number : cardData.number,
cvc : cardData.cvc,
exp_month : cardData.month,
exp_year : cardData.year
};
Stripe.createToken(submitData, stripeResponseHandler);
return false;
}
var stripeResponseHandler = function(status, response) {
if (response.error) {
var errors = [];
switch (response.error.type) {
case 'card_error':
var error = {};
error[response.error.code] = response.error.message;
errors.push(error);
break;
case 'invalid_request_error':
errors.push(stripeErrorObject('invalid_request'));
break;
case 'api_error':
default:
errors.push(stripeErrorObject('stripe'));
break;
}
cardErrors.value = JX.JSON.stringify(errors);
} else {
// success - we can use the token to create a customer object with
// Stripe and let the billing commence!
var token = response['id'];
stripeToken.value = token;
}
root.submit();
}
JX.DOM.listen(
root,
'submit',
null,
onsubmit);
});

1
webroot/rsrc/js/stripe Symbolic link
View file

@ -0,0 +1 @@
../../../externals/stripe-js/