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

Genericize "Add Payment Method" form

Summary:
Ref T2787. For payment methods that allow you to add a billable method (i.e., a credit card), move all the logic into the provider. In particular:

  - Providers may (Stripe, Balanced) or may not (Paypal, MtGox) allow you to add rebillable payment methods. Providers which don't allow rebillable methods will appear at checkout instead and we'll just invoice you every month if you don't use a rebillable method.
  - Providers which permit creation of rebillable methods handle their own data entry, since this will be per-provider.
  - "Add Payment Method" now prompts you to choose a provider. This is super ugly and barely-usable for the moment. When there's only one choice, we'll auto-select it in the future.

Test Plan: Added new Stripe payment methods; hit all the Stripe errors.

Reviewers: btrahan, chad

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D5756
This commit is contained in:
epriestley 2013-04-25 09:46:32 -07:00
parent f790a2aeec
commit 9c43029277
11 changed files with 511 additions and 296 deletions

View file

@ -2258,7 +2258,7 @@ celerity_register_resource_map(array(
), ),
'javelin-behavior-stripe-payment-form' => 'javelin-behavior-stripe-payment-form' =>
array( array(
'uri' => '/res/30bcbbb1/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 'uri' => '/res/e4149d37/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
'type' => 'js', 'type' => 'js',
'requires' => 'requires' =>
array( array(
@ -2266,7 +2266,6 @@ celerity_register_resource_map(array(
1 => 'javelin-dom', 1 => 'javelin-dom',
2 => 'javelin-json', 2 => 'javelin-json',
3 => 'javelin-workflow', 3 => 'javelin-workflow',
4 => 'stripe-core',
), ),
'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
), ),

View file

@ -9,6 +9,15 @@ final class DarkConsoleErrorLogPluginAPI {
private static $discardMode = false; private static $discardMode = false;
public static function registerErrorHandler() {
// NOTE: This forces PhutilReadableSerializer to load, so that we are
// able to handle errors which fire from inside autoloaders (PHP will not
// reenter autoloaders).
PhutilReadableSerializer::printableValue(null);
PhutilErrorHandler::setErrorListener(
array('DarkConsoleErrorLogPluginAPI', 'handleErrors'));
}
public static function enableDiscardMode() { public static function enableDiscardMode() {
self::$discardMode = true; self::$discardMode = true;
} }

View file

@ -9,28 +9,10 @@ final class PhortunePaymentMethodEditController
$this->accountID = $data['accountID']; $this->accountID = $data['accountID'];
} }
/**
* @phutil-external-symbol class Stripe_Token
* @phutil-external-symbol class Stripe_Customer
*/
public function processRequest() { public function processRequest() {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$stripe_publishable_key = PhabricatorEnv::getEnvConfig(
'stripe.publishable-key');
if (!$stripe_publishable_key) {
throw new Exception(
"Stripe publishable API key (`stripe.publishable-key`) is ".
"not configured.");
}
$stripe_secret_key = PhabricatorEnv::getEnvConfig('stripe.secret-key');
if (!$stripe_secret_key) {
throw new Exception(
"Stripe secret API kye (`stripe.secret-key`) is not configured.");
}
$account = id(new PhortuneAccountQuery()) $account = id(new PhortuneAccountQuery())
->setViewer($user) ->setViewer($user)
->withIDs(array($this->accountID)) ->withIDs(array($this->accountID))
@ -39,171 +21,113 @@ final class PhortunePaymentMethodEditController
return new Aphront404Response(); return new Aphront404Response();
} }
$cancel_uri = $this->getApplicationURI($account->getID().'/');
$account_uri = $this->getApplicationURI($account->getID().'/'); $account_uri = $this->getApplicationURI($account->getID().'/');
$e_card_number = true; $providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod();
$e_card_cvc = true; if (!$providers) {
$e_card_exp = true; throw new Exception(
"There are no payment providers enabled that can add payment ".
"methods.");
}
$provider_key = $request->getStr('providerKey');
if (empty($providers[$provider_key])) {
$choices = array();
foreach ($providers as $provider) {
$choices[] = $this->renderSelectProvider($provider);
}
return $this->buildResponse($choices, $account_uri);
}
$provider = $providers[$provider_key];
$errors = array(); $errors = array();
if ($request->isFormPost()) { if ($request->isFormPost() && $request->getBool('isProviderForm')) {
$card_errors = $request->getStr('cardErrors'); $method = id(new PhortunePaymentMethod())
$stripe_token = $request->getStr('stripeToken'); ->setAccountPHID($account->getPHID())
if ($card_errors) { ->setAuthorPHID($user->getPHID())
$raw_errors = json_decode($card_errors); ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE)
list($e_card_number, ->setMetadataValue('providerKey', $provider->getProviderKey());
$e_card_cvc,
$e_card_exp, $errors = $provider->createPaymentMethodFromRequest($request, $method);
$messages) = $this->parseRawErrors($raw_errors);
$errors = array_merge($errors, $messages);
} else if (!$stripe_token) {
$errors[] = pht('There was an unknown error processing your card.');
}
if (!$errors) { if (!$errors) {
$root = dirname(phutil_get_library_root('phabricator')); $method->save();
require_once $root.'/externals/stripe-php/lib/Stripe.php';
try { $save_uri = new PhutilURI($account_uri);
// First, make sure the token is valid. $save_uri->setFragment('payment');
$info = id(new Stripe_Token()) return id(new AphrontRedirectResponse())->setURI($save_uri);
->retrieve($stripe_token, $stripe_secret_key); } else {
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Error Adding Payment Method'))
->appendChild(id(new AphrontErrorView())->setErrors($errors))
->addCancelButton($request->getRequestURI());
// Then, we need to create a Customer in order to be able to charge return id(new AphrontDialogResponse())->setDialog($dialog);
// the card more than once. We create one Customer for each card;
// they do not map to PhortuneAccounts because we allow an account to
// have more than one active card.
$customer = Stripe_Customer::create(
array(
'card' => $stripe_token,
'description' => $account->getPHID().':'.$user->getUserName(),
), $stripe_secret_key);
$card = $info->card;
} catch (Exception $ex) {
phlog($ex);
$errors[] = pht(
'There was an error communicating with the payments backend.');
}
if (!$errors) {
$payment_method = id(new PhortunePaymentMethod())
->setAccountPHID($account->getPHID())
->setAuthorPHID($user->getPHID())
->setName($card->type.' / '.$card->last4)
->setStatus(PhortunePaymentMethod::STATUS_ACTIVE)
->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month))
->setMetadata(
array(
'type' => 'stripe.customer',
'stripe.customerID' => $customer->id,
'stripe.tokenID' => $stripe_token,
))
->save();
$save_uri = new PhutilURI($account_uri);
$save_uri->setFragment('payment');
return id(new AphrontRedirectResponse())->setURI($save_uri);
}
} }
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Error Adding Card'))
->appendChild(id(new AphrontErrorView())->setErrors($errors))
->addCancelButton($request->getRequestURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
} }
$form = $provider->renderCreatePaymentMethodForm($request, $errors);
$form
->setUser($user)
->setAction($request->getRequestURI())
->setWorkflow(true)
->addHiddenInput('providerKey', $provider_key)
->addHiddenInput('isProviderForm', true)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Add Payment Method'))
->addCancelButton($account_uri));
if ($errors) { if ($errors) {
$errors = id(new AphrontErrorView()) $errors = id(new AphrontErrorView())
->setErrors($errors); ->setErrors($errors);
} }
$header = id(new PhabricatorHeaderView()) return $this->buildResponse(
->setHeader(pht('Add New Payment Method')); array($errors, $form),
$account_uri);
}
$form_id = celerity_generate_unique_node_id(); private function renderSelectProvider(
require_celerity_resource('stripe-payment-form-css'); PhortunePaymentProvider $provider) {
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips');
$form = id(new AphrontFormView()) $request = $this->getRequest();
->setID($form_id) $user = $request->getUser();
->setUser($user)
->setWorkflow(true)
->setAction($request->getRequestURI())
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('')
->setValue(
javelin_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($e_card_number))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('CVC')
->setDisableAutocomplete(true)
->setSigil('cvc-input')
->setError($e_card_cvc))
->appendChild(
id(new PhortuneMonthYearExpiryControl())
->setLabel('Expiration')
->setUser($user)
->setError($e_card_exp))
->appendChild(
javelin_tag(
'input',
array(
'hidden' => true,
'name' => 'stripeToken',
'sigil' => 'stripe-token-input',
)))
->appendChild(
javelin_tag(
'input',
array(
'hidden' => true,
'name' => 'cardErrors',
'sigil' => 'card-errors-input'
)))
->appendChild(
phutil_tag(
'input',
array(
'hidden' => true,
'name' => 'stripeKey',
'value' => $stripe_publishable_key,
)))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Add Payment Method')
->addCancelButton($account_uri));
Javelin::initBehavior( $description = $provider->getPaymentMethodDescription();
'stripe-payment-form', $icon = $provider->getPaymentMethodIcon();
$details = $provider->getPaymentMethodProviderDescription();
$button = phutil_tag(
'button',
array( array(
'stripePublishKey' => $stripe_publishable_key, 'class' => 'grey',
'root' => $form_id, ),
array(
$description,
phutil_tag('br'),
$icon,
$details,
)); ));
$form = id(new AphrontFormView())
->setUser($user)
->addHiddenInput('providerKey', $provider->getProviderKey())
->appendChild($button);
return $form;
}
private function buildResponse($content, $account_uri) {
$request = $this->getRequest();
$title = pht('Add Payment Method'); $title = pht('Add Payment Method');
$header = id(new PhabricatorHeaderView())
->setHeader($title);
$crumbs = $this->buildApplicationCrumbs(); $crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb( $crumbs->addCrumb(
@ -215,84 +139,17 @@ final class PhortunePaymentMethodEditController
->setName(pht('Payment Methods')) ->setName(pht('Payment Methods'))
->setHref($request->getRequestURI())); ->setHref($request->getRequestURI()));
return return $this->buildApplicationPage(
$this->buildStandardPageResponse( array(
array( $crumbs,
$crumbs, $header,
$header, $content,
$errors, ),
$form, array(
), 'title' => $title,
array( 'device' => true,
'title' => $title, 'dust' => true,
'device' => true, ));
'dust' => true,
));
}
/**
* 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 = pht('Invalid');
break;
case 'cvc':
case 'invalid_cvc':
case 'incorrect_cvc':
$card_cvc_error = pht('Invalid');
break;
case 'expiry':
case 'invalid_expiry_month':
case 'invalid_expiry_year':
$card_expiration_error = pht('Invalid');
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;
}
}
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,11 @@
<?php
final class PhortuneNotImplementedException extends Exception {
public function __construct(PhortunePaymentProvider $provider) {
$class = get_class($provider);
return parent::__construct(
"Provider '{$provider}' does not implement this method.");
}
}

View file

@ -1,7 +1,72 @@
<?php <?php
/**
* @task addmethod Adding Payment Methods
*/
abstract class PhortunePaymentProvider { abstract class PhortunePaymentProvider {
/* -( Selecting Providers )------------------------------------------------ */
public static function getAllProviders() {
$objects = id(new PhutilSymbolLoader())
->setAncestorClass('PhortunePaymentProvider')
->loadObjects();
return mpull($objects, null, 'getProviderKey');
}
public static function getEnabledProviders() {
$providers = self::getAllProviders();
foreach ($providers as $key => $provider) {
if (!$provider->isEnabled()) {
unset($providers[$key]);
}
}
return $providers;
}
public static function getProvidersForAddPaymentMethod() {
$providers = self::getEnabledProviders();
foreach ($providers as $key => $provider) {
if (!$provider->canCreatePaymentMethods()) {
unset($providers[$key]);
}
}
return $providers;
}
abstract public function isEnabled();
final public function getProviderKey() {
return $this->getProviderType().'@'.$this->getProviderDomain();
}
/**
* Return a short string which uniquely identifies this provider's protocol
* type, like "stripe", "paypal", or "balanced".
*/
abstract public function getProviderType();
/**
* Return a short string which uniquely identifies the domain for this
* provider, like "stripe.com" or "google.com".
*
* This is distinct from the provider type so that protocols are not bound
* to a single domain. This is probably not relevant for payments, but this
* assumption burned us pretty hard with authentication and it's easy enough
* to avoid.
*/
abstract public function getProviderDomain();
abstract public function getPaymentMethodDescription();
abstract public function getPaymentMethodIcon();
abstract public function getPaymentMethodProviderDescription();
/** /**
* Determine of a provider can handle a payment method. * Determine of a provider can handle a payment method.
* *
@ -15,4 +80,36 @@ abstract class PhortunePaymentProvider {
PhortunePaymentMethod $payment_method, PhortunePaymentMethod $payment_method,
PhortuneCharge $charge); PhortuneCharge $charge);
/* -( Adding Payment Methods )--------------------------------------------- */
/**
* @task addmethod
*/
public function canCreatePaymentMethods() {
return false;
}
/**
* @task addmethod
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
throw new PhortuneNotImplementedException($this);
}
} }

View file

@ -2,6 +2,32 @@
final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
public function isEnabled() {
return $this->getPublishableKey() &&
$this->getSecretKey();
}
public function getProviderType() {
return 'stripe';
}
public function getProviderDomain() {
return 'stripe.com';
}
public function getPaymentMethodDescription() {
return pht('Add Credit or Debit Card (US and Canada)');
}
public function getPaymentMethodIcon() {
return celerity_get_resource_uri('/rsrc/image/phortune/stripe.png');
}
public function getPaymentMethodProviderDescription() {
return pht('Processed by Stripe');
}
public function canHandlePaymentMethod(PhortunePaymentMethod $method) { public function canHandlePaymentMethod(PhortunePaymentMethod $method) {
$type = $method->getMetadataValue('type'); $type = $method->getMetadataValue('type');
return ($type === 'stripe.customer'); return ($type === 'stripe.customer');
@ -32,8 +58,219 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
$charge->setMetadataValue('stripe.chargeID', $id); $charge->setMetadataValue('stripe.chargeID', $id);
} }
private function getPublishableKey() {
return PhabricatorEnv::getEnvConfig('stripe.publishable-key');
}
private function getSecretKey() { private function getSecretKey() {
return PhabricatorEnv::getEnvConfig('stripe.secret-key'); return PhabricatorEnv::getEnvConfig('stripe.secret-key');
} }
/* -( Adding Payment Methods )--------------------------------------------- */
public function canCreatePaymentMethods() {
return true;
}
/**
* @phutil-external-symbol class Stripe_Token
* @phutil-external-symbol class Stripe_Customer
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method) {
$card_errors = $request->getStr('cardErrors');
$stripe_token = $request->getStr('stripeToken');
if ($card_errors) {
$raw_errors = json_decode($card_errors);
$errors = $this->parseRawCreatePaymentMethodErrors($raw_errors);
} else if (!$stripe_token) {
$errors[] = pht('There was an unknown error processing your card.');
}
$secret_key = $this->getSecretKey();
if (!$errors) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
try {
// First, make sure the token is valid.
$info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$params = array(
'card' => $stripe_token,
'description' => $account_phid.':'.$author_phid,
);
// Then, we need to create a Customer in order to be able to charge
// the card more than once. We create one Customer for each card;
// they do not map to PhortuneAccounts because we allow an account to
// have more than one active card.
$customer = Stripe_Customer::create($params, $secret_key);
$card = $info->card;
$method
->setName($card->type.' / '.$card->last4)
->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month))
->setMetadata(
array(
'type' => 'stripe.customer',
'stripe.customerID' => $customer->id,
'stripe.tokenID' => $stripe_token,
));
} catch (Exception $ex) {
phlog($ex);
$errors[] = pht(
'There was an error communicating with the payments backend.');
}
}
return $errors;
}
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
$e_card_number = isset($errors['number']) ? pht('Invalid') : true;
$e_card_cvc = isset($errors['cvc']) ? pht('Invalid') : true;
$e_card_exp = isset($errors['exp']) ? pht('Invalid') : null;
$user = $request->getUser();
$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)
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('')
->setValue(
javelin_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($e_card_number))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('CVC')
->setDisableAutocomplete(true)
->setSigil('cvc-input')
->setError($e_card_cvc))
->appendChild(
id(new PhortuneMonthYearExpiryControl())
->setLabel('Expiration')
->setUser($user)
->setError($e_card_exp))
->appendChild(
javelin_tag(
'input',
array(
'hidden' => true,
'name' => 'stripeToken',
'sigil' => 'stripe-token-input',
)))
->appendChild(
javelin_tag(
'input',
array(
'hidden' => true,
'name' => 'cardErrors',
'sigil' => 'card-errors-input'
)));
require_celerity_resource('stripe-core');
Javelin::initBehavior(
'stripe-payment-form',
array(
'stripePublishKey' => $this->getPublishableKey(),
'root' => $form_id,
));
return $form;
}
/**
* 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 parseRawCreatePaymentMethodErrors(array $raw_errors) {
$errors = array();
foreach ($raw_errors as $type) {
$error_key = null;
$message = pht('A card processing error has occurred.');
switch ($type) {
case 'number':
case 'invalid_number':
case 'incorrect_number':
$error_key = 'number';
$message = pht('Invalid or incorrect credit card number.');
break;
case 'cvc':
case 'invalid_cvc':
case 'incorrect_cvc':
$error_key = 'cvc';
$message = pht('Card CVC is invalid or incorrect.');
break;
case 'expiry':
case 'invalid_expiry_month':
case 'invalid_expiry_year':
$error_key = 'exp';
$message = pht('Card expiration date is invalid or incorrect.');
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('[Stripe Error] %s', $type);
phlog($error);
break;
}
if ($error_key === null || isset($errors[$error_key])) {
$errors[] = $message;
} else {
$errors[$error_key] = $message;
}
}
return $errors;
}
} }

View file

@ -2,6 +2,30 @@
final class PhortuneTestPaymentProvider extends PhortunePaymentProvider { final class PhortuneTestPaymentProvider extends PhortunePaymentProvider {
public function isEnabled() {
return true;
}
public function getProviderType() {
return 'test';
}
public function getProviderDomain() {
return 'example.com';
}
public function getPaymentMethodDescription() {
return pht('Add Mountain of Virtual Wealth');
}
public function getPaymentMethodIcon() {
return celerity_get_resource_uri('/rsrc/image/phortune/test.png');
}
public function getPaymentMethodProviderDescription() {
return pht('Infinite Free Money');
}
public function canHandlePaymentMethod(PhortunePaymentMethod $method) { public function canHandlePaymentMethod(PhortunePaymentMethod $method) {
$type = $method->getMetadataValue('type'); $type = $method->getMetadataValue('type');
return ($type === 'test.cash' || $type === 'test.multiple'); return ($type === 'test.cash' || $type === 'test.multiple');

View file

@ -2,6 +2,30 @@
final class PhortuneTestExtraPaymentProvider extends PhortunePaymentProvider { final class PhortuneTestExtraPaymentProvider extends PhortunePaymentProvider {
public function isEnabled() {
return false;
}
public function getProviderType() {
return 'test2';
}
public function getProviderDomain() {
return 'example.com';
}
public function getPaymentMethodDescription() {
return pht('You Should Not Be Able to See This');
}
public function getPaymentMethodIcon() {
return celerity_get_resource_uri('/rsrc/image/phortune/test.png');
}
public function getPaymentMethodProviderDescription() {
return pht('Just for Unit Tests');
}
public function canHandlePaymentMethod(PhortunePaymentMethod $method) { public function canHandlePaymentMethod(PhortunePaymentMethod $method) {
$type = $method->getMetadataValue('type'); $type = $method->getMetadataValue('type');
return ($type === 'test.multiple'); return ($type === 'test.multiple');

View file

@ -60,16 +60,12 @@ final class PhortunePaymentMethod extends PhortuneDAO
} }
public function buildPaymentProvider() { public function buildPaymentProvider() {
$providers = id(new PhutilSymbolLoader()) $providers = PhortunePaymentProvider::getAllProviders();
->setAncestorClass('PhortunePaymentProvider')
->setConcreteOnly(true)
->selectAndLoadSymbols();
$accept = array(); $accept = array();
foreach ($providers as $provider) { foreach ($providers as $provider) {
$obj = newv($provider['name'], array()); if ($provider->canHandlePaymentMethod($this)) {
if ($obj->canHandlePaymentMethod($this)) { $accept[] = $provider;
$accept[] = $obj;
} }
} }

View file

@ -20,9 +20,7 @@ try {
)); ));
DarkConsoleXHProfPluginAPI::hookProfiler(); DarkConsoleXHProfPluginAPI::hookProfiler();
DarkConsoleErrorLogPluginAPI::registerErrorHandler();
PhutilErrorHandler::setErrorListener(
array('DarkConsoleErrorLogPluginAPI', 'handleErrors'));
$sink = new AphrontPHPHTTPSink(); $sink = new AphrontPHPHTTPSink();

View file

@ -4,7 +4,6 @@
* javelin-dom * javelin-dom
* javelin-json * javelin-json
* javelin-workflow * javelin-workflow
* stripe-core
*/ */
JX.behavior('stripe-payment-form', function(config) { JX.behavior('stripe-payment-form', function(config) {
@ -23,38 +22,6 @@ JX.behavior('stripe-payment-form', function(config) {
}; };
} }
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) { var onsubmit = function(e) {
e.kill(); e.kill();
@ -63,21 +30,19 @@ JX.behavior('stripe-payment-form', function(config) {
var cardData = getCardData(); var cardData = getCardData();
var errors = []; var errors = [];
if (!Stripe.validateCardNumber(cardData.number)) { if (!Stripe.validateCardNumber(cardData.number)) {
errors.push(stripeErrorObject('number')); errors.push('number');
} }
if (!Stripe.validateCVC(cardData.cvc)) { if (!Stripe.validateCVC(cardData.cvc)) {
errors.push(stripeErrorObject('cvc')); errors.push('cvc');
} }
if (!Stripe.validateExpiry(cardData.month, if (!Stripe.validateExpiry(cardData.month, cardData.year)) {
cardData.year)) { errors.push('expiry');
errors.push(stripeErrorObject('expiry'));
} }
if (errors.length != 0) {
cardErrors.value = JX.JSON.stringify(errors);
if (errors.length) {
cardErrors.value = JX.JSON.stringify(errors);
JX.Workflow.newFromForm(root) JX.Workflow.newFromForm(root)
.start(); .start();
return; return;
} }
@ -97,16 +62,14 @@ JX.behavior('stripe-payment-form', function(config) {
var errors = []; var errors = [];
switch (response.error.type) { switch (response.error.type) {
case 'card_error': case 'card_error':
var error = {}; errors.push(response.error.code);
error[response.error.code] = response.error.message;
errors.push(error);
break; break;
case 'invalid_request_error': case 'invalid_request_error':
errors.push(stripeErrorObject('invalid_request')); errors.push('invalid_request');
break; break;
case 'api_error': case 'api_error':
default: default:
errors.push(stripeErrorObject('stripe')); errors.push('stripe');
break; break;
} }
cardErrors.value = JX.JSON.stringify(errors); cardErrors.value = JX.JSON.stringify(errors);