mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-29 10:12:41 +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:
parent
f790a2aeec
commit
9c43029277
11 changed files with 511 additions and 296 deletions
|
@ -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',
|
||||||
),
|
),
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
|
||||||
if ($card_errors) {
|
|
||||||
$raw_errors = json_decode($card_errors);
|
|
||||||
list($e_card_number,
|
|
||||||
$e_card_cvc,
|
|
||||||
$e_card_exp,
|
|
||||||
$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) {
|
|
||||||
$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, $stripe_secret_key);
|
|
||||||
|
|
||||||
// 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(
|
|
||||||
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())
|
->setAccountPHID($account->getPHID())
|
||||||
->setAuthorPHID($user->getPHID())
|
->setAuthorPHID($user->getPHID())
|
||||||
->setName($card->type.' / '.$card->last4)
|
|
||||||
->setStatus(PhortunePaymentMethod::STATUS_ACTIVE)
|
->setStatus(PhortunePaymentMethod::STATUS_ACTIVE)
|
||||||
->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month))
|
->setMetadataValue('providerKey', $provider->getProviderKey());
|
||||||
->setMetadata(
|
|
||||||
array(
|
$errors = $provider->createPaymentMethodFromRequest($request, $method);
|
||||||
'type' => 'stripe.customer',
|
|
||||||
'stripe.customerID' => $customer->id,
|
if (!$errors) {
|
||||||
'stripe.tokenID' => $stripe_token,
|
$method->save();
|
||||||
))
|
|
||||||
->save();
|
|
||||||
|
|
||||||
$save_uri = new PhutilURI($account_uri);
|
$save_uri = new PhutilURI($account_uri);
|
||||||
$save_uri->setFragment('payment');
|
$save_uri->setFragment('payment');
|
||||||
|
|
||||||
return id(new AphrontRedirectResponse())->setURI($save_uri);
|
return id(new AphrontRedirectResponse())->setURI($save_uri);
|
||||||
}
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
$dialog = id(new AphrontDialogView())
|
$dialog = id(new AphrontDialogView())
|
||||||
->setUser($user)
|
->setUser($user)
|
||||||
->setTitle(pht('Error Adding Card'))
|
->setTitle(pht('Error Adding Payment Method'))
|
||||||
->appendChild(id(new AphrontErrorView())->setErrors($errors))
|
->appendChild(id(new AphrontErrorView())->setErrors($errors))
|
||||||
->addCancelButton($request->getRequestURI());
|
->addCancelButton($request->getRequestURI());
|
||||||
|
|
||||||
return id(new AphrontDialogResponse())->setDialog($dialog);
|
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,13 +139,11 @@ 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,
|
||||||
$errors,
|
$content,
|
||||||
$form,
|
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
|
@ -230,69 +152,4 @@ final class PhortunePaymentMethodEditController
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,7 @@ try {
|
||||||
));
|
));
|
||||||
|
|
||||||
DarkConsoleXHProfPluginAPI::hookProfiler();
|
DarkConsoleXHProfPluginAPI::hookProfiler();
|
||||||
|
DarkConsoleErrorLogPluginAPI::registerErrorHandler();
|
||||||
PhutilErrorHandler::setErrorListener(
|
|
||||||
array('DarkConsoleErrorLogPluginAPI', 'handleErrors'));
|
|
||||||
|
|
||||||
$sink = new AphrontPHPHTTPSink();
|
$sink = new AphrontPHPHTTPSink();
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue