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

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');