From 9c43029277609d44168d3b5210b35054821c4e4e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 25 Apr 2013 09:46:32 -0700 Subject: [PATCH] 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 --- src/__celerity_resource_map__.php | 3 +- .../errorlog/DarkConsoleErrorLogPluginAPI.php | 9 + .../PhortunePaymentMethodEditController.php | 331 +++++------------- .../PhortuneNotImplementedException.php | 11 + .../provider/PhortunePaymentProvider.php | 97 +++++ .../PhortuneStripePaymentProvider.php | 237 +++++++++++++ .../provider/PhortuneTestPaymentProvider.php | 24 ++ .../PhortuneTestExtraPaymentProvider.php | 24 ++ .../storage/PhortunePaymentMethod.php | 10 +- webroot/index.php | 4 +- .../phortune/behavior-stripe-payment-form.js | 55 +-- 11 files changed, 510 insertions(+), 295 deletions(-) create mode 100644 src/applications/phortune/exception/PhortuneNotImplementedException.php diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 6e03e6cbda..7dcf758313 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -2258,7 +2258,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-stripe-payment-form' => 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', 'requires' => array( @@ -2266,7 +2266,6 @@ celerity_register_resource_map(array( 1 => 'javelin-dom', 2 => 'javelin-json', 3 => 'javelin-workflow', - 4 => 'stripe-core', ), 'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js', ), diff --git a/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php b/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php index fff59aab6d..3855dd4a89 100644 --- a/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php +++ b/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php @@ -9,6 +9,15 @@ final class DarkConsoleErrorLogPluginAPI { 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() { self::$discardMode = true; } diff --git a/src/applications/phortune/controller/PhortunePaymentMethodEditController.php b/src/applications/phortune/controller/PhortunePaymentMethodEditController.php index fdc8f13151..4623ea67d1 100644 --- a/src/applications/phortune/controller/PhortunePaymentMethodEditController.php +++ b/src/applications/phortune/controller/PhortunePaymentMethodEditController.php @@ -9,28 +9,10 @@ final class PhortunePaymentMethodEditController $this->accountID = $data['accountID']; } - /** - * @phutil-external-symbol class Stripe_Token - * @phutil-external-symbol class Stripe_Customer - */ public function processRequest() { $request = $this->getRequest(); $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()) ->setViewer($user) ->withIDs(array($this->accountID)) @@ -39,171 +21,113 @@ final class PhortunePaymentMethodEditController return new Aphront404Response(); } + $cancel_uri = $this->getApplicationURI($account->getID().'/'); $account_uri = $this->getApplicationURI($account->getID().'/'); - $e_card_number = true; - $e_card_cvc = true; - $e_card_exp = true; + $providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod(); + if (!$providers) { + throw new Exception( + "There are no payment providers enabled that can add payment ". + "methods."); + } - $errors = array(); - if ($request->isFormPost()) { - $card_errors = $request->getStr('cardErrors'); - $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.'); + $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); + } - 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.'); - } + $provider = $providers[$provider_key]; - 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(); + $errors = array(); + if ($request->isFormPost() && $request->getBool('isProviderForm')) { + $method = id(new PhortunePaymentMethod()) + ->setAccountPHID($account->getPHID()) + ->setAuthorPHID($user->getPHID()) + ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE) + ->setMetadataValue('providerKey', $provider->getProviderKey()); - $save_uri = new PhutilURI($account_uri); - $save_uri->setFragment('payment'); + $errors = $provider->createPaymentMethodFromRequest($request, $method); - return id(new AphrontRedirectResponse())->setURI($save_uri); - } + if (!$errors) { + $method->save(); + + $save_uri = new PhutilURI($account_uri); + $save_uri->setFragment('payment'); + return id(new AphrontRedirectResponse())->setURI($save_uri); + } else { + $dialog = id(new AphrontDialogView()) + ->setUser($user) + ->setTitle(pht('Error Adding Payment Method')) + ->appendChild(id(new AphrontErrorView())->setErrors($errors)) + ->addCancelButton($request->getRequestURI()); + + return id(new AphrontDialogResponse())->setDialog($dialog); } + } - $dialog = id(new AphrontDialogView()) - ->setUser($user) - ->setTitle(pht('Error Adding Card')) - ->appendChild(id(new AphrontErrorView())->setErrors($errors)) - ->addCancelButton($request->getRequestURI()); + $form = $provider->renderCreatePaymentMethodForm($request, $errors); - return id(new AphrontDialogResponse())->setDialog($dialog); - } + $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) { $errors = id(new AphrontErrorView()) ->setErrors($errors); } - $header = id(new PhabricatorHeaderView()) - ->setHeader(pht('Add New Payment Method')); + return $this->buildResponse( + array($errors, $form), + $account_uri); + } - $form_id = celerity_generate_unique_node_id(); - require_celerity_resource('stripe-payment-form-css'); - require_celerity_resource('aphront-tooltip-css'); - Javelin::initBehavior('phabricator-tooltips'); + private function renderSelectProvider( + PhortunePaymentProvider $provider) { - $form = id(new AphrontFormView()) - ->setID($form_id) - ->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)); + $request = $this->getRequest(); + $user = $request->getUser(); - Javelin::initBehavior( - 'stripe-payment-form', + $description = $provider->getPaymentMethodDescription(); + $icon = $provider->getPaymentMethodIcon(); + $details = $provider->getPaymentMethodProviderDescription(); + + $button = phutil_tag( + 'button', + array( + 'class' => 'grey', + ), array( - 'stripePublishKey' => $stripe_publishable_key, - 'root' => $form_id, + $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'); + $header = id(new PhabricatorHeaderView()) + ->setHeader($title); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( @@ -215,84 +139,17 @@ final class PhortunePaymentMethodEditController ->setName(pht('Payment Methods')) ->setHref($request->getRequestURI())); - return - $this->buildStandardPageResponse( - array( - $crumbs, - $header, - $errors, - $form, - ), - array( - 'title' => $title, - '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); + return $this->buildApplicationPage( + array( + $crumbs, + $header, + $content, + ), + array( + 'title' => $title, + 'device' => true, + 'dust' => true, + )); } } diff --git a/src/applications/phortune/exception/PhortuneNotImplementedException.php b/src/applications/phortune/exception/PhortuneNotImplementedException.php new file mode 100644 index 0000000000..89a2258b18 --- /dev/null +++ b/src/applications/phortune/exception/PhortuneNotImplementedException.php @@ -0,0 +1,11 @@ +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. * @@ -15,4 +80,36 @@ abstract class PhortunePaymentProvider { PhortunePaymentMethod $payment_method, 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); + } + } diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php index 5d26b15393..8066eba1f4 100644 --- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php @@ -2,6 +2,32 @@ 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) { $type = $method->getMetadataValue('type'); return ($type === 'stripe.customer'); @@ -32,8 +58,219 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { $charge->setMetadataValue('stripe.chargeID', $id); } + private function getPublishableKey() { + return PhabricatorEnv::getEnvConfig('stripe.publishable-key'); + } + private function getSecretKey() { 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; + } + } diff --git a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php index 059c8c16c3..3ab4cb7a8b 100644 --- a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php @@ -2,6 +2,30 @@ 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) { $type = $method->getMetadataValue('type'); return ($type === 'test.cash' || $type === 'test.multiple'); diff --git a/src/applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php b/src/applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php index 3453ea7552..5ea6376750 100644 --- a/src/applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php +++ b/src/applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php @@ -2,6 +2,30 @@ 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) { $type = $method->getMetadataValue('type'); return ($type === 'test.multiple'); diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php index 5293be4ff7..2df720188c 100644 --- a/src/applications/phortune/storage/PhortunePaymentMethod.php +++ b/src/applications/phortune/storage/PhortunePaymentMethod.php @@ -60,16 +60,12 @@ final class PhortunePaymentMethod extends PhortuneDAO } public function buildPaymentProvider() { - $providers = id(new PhutilSymbolLoader()) - ->setAncestorClass('PhortunePaymentProvider') - ->setConcreteOnly(true) - ->selectAndLoadSymbols(); + $providers = PhortunePaymentProvider::getAllProviders(); $accept = array(); foreach ($providers as $provider) { - $obj = newv($provider['name'], array()); - if ($obj->canHandlePaymentMethod($this)) { - $accept[] = $obj; + if ($provider->canHandlePaymentMethod($this)) { + $accept[] = $provider; } } diff --git a/webroot/index.php b/webroot/index.php index 5a9479eab1..7ee22338cc 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -20,9 +20,7 @@ try { )); DarkConsoleXHProfPluginAPI::hookProfiler(); - - PhutilErrorHandler::setErrorListener( - array('DarkConsoleErrorLogPluginAPI', 'handleErrors')); + DarkConsoleErrorLogPluginAPI::registerErrorHandler(); $sink = new AphrontPHPHTTPSink(); diff --git a/webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js b/webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js index 23645be1c5..4c06828dd2 100644 --- a/webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js +++ b/webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js @@ -4,7 +4,6 @@ * javelin-dom * javelin-json * javelin-workflow - * stripe-core */ 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) { e.kill(); @@ -63,21 +30,19 @@ JX.behavior('stripe-payment-form', function(config) { var cardData = getCardData(); var errors = []; if (!Stripe.validateCardNumber(cardData.number)) { - errors.push(stripeErrorObject('number')); + errors.push('number'); } if (!Stripe.validateCVC(cardData.cvc)) { - errors.push(stripeErrorObject('cvc')); + errors.push('cvc'); } - if (!Stripe.validateExpiry(cardData.month, - cardData.year)) { - errors.push(stripeErrorObject('expiry')); + if (!Stripe.validateExpiry(cardData.month, cardData.year)) { + errors.push('expiry'); } - if (errors.length != 0) { - cardErrors.value = JX.JSON.stringify(errors); + if (errors.length) { + cardErrors.value = JX.JSON.stringify(errors); JX.Workflow.newFromForm(root) .start(); - return; } @@ -97,16 +62,14 @@ JX.behavior('stripe-payment-form', function(config) { var errors = []; switch (response.error.type) { case 'card_error': - var error = {}; - error[response.error.code] = response.error.message; - errors.push(error); + errors.push(response.error.code); break; case 'invalid_request_error': - errors.push(stripeErrorObject('invalid_request')); + errors.push('invalid_request'); break; case 'api_error': default: - errors.push(stripeErrorObject('stripe')); + errors.push('stripe'); break; } cardErrors.value = JX.JSON.stringify(errors); -- 2.54.0