@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+508 -293
+1 -2
src/__celerity_resource_map__.php
··· 2258 2258 ), 2259 2259 'javelin-behavior-stripe-payment-form' => 2260 2260 array( 2261 - 'uri' => '/res/30bcbbb1/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2261 + 'uri' => '/res/e4149d37/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2262 2262 'type' => 'js', 2263 2263 'requires' => 2264 2264 array( ··· 2266 2266 1 => 'javelin-dom', 2267 2267 2 => 'javelin-json', 2268 2268 3 => 'javelin-workflow', 2269 - 4 => 'stripe-core', 2270 2269 ), 2271 2270 'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2272 2271 ),
+9
src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
··· 9 9 10 10 private static $discardMode = false; 11 11 12 + public static function registerErrorHandler() { 13 + // NOTE: This forces PhutilReadableSerializer to load, so that we are 14 + // able to handle errors which fire from inside autoloaders (PHP will not 15 + // reenter autoloaders). 16 + PhutilReadableSerializer::printableValue(null); 17 + PhutilErrorHandler::setErrorListener( 18 + array('DarkConsoleErrorLogPluginAPI', 'handleErrors')); 19 + } 20 + 12 21 public static function enableDiscardMode() { 13 22 self::$discardMode = true; 14 23 }
+92 -235
src/applications/phortune/controller/PhortunePaymentMethodEditController.php
··· 9 9 $this->accountID = $data['accountID']; 10 10 } 11 11 12 - /** 13 - * @phutil-external-symbol class Stripe_Token 14 - * @phutil-external-symbol class Stripe_Customer 15 - */ 16 12 public function processRequest() { 17 13 $request = $this->getRequest(); 18 14 $user = $request->getUser(); 19 15 20 - $stripe_publishable_key = PhabricatorEnv::getEnvConfig( 21 - 'stripe.publishable-key'); 22 - if (!$stripe_publishable_key) { 23 - throw new Exception( 24 - "Stripe publishable API key (`stripe.publishable-key`) is ". 25 - "not configured."); 26 - } 27 - 28 - $stripe_secret_key = PhabricatorEnv::getEnvConfig('stripe.secret-key'); 29 - if (!$stripe_secret_key) { 30 - throw new Exception( 31 - "Stripe secret API kye (`stripe.secret-key`) is not configured."); 32 - } 33 - 34 16 $account = id(new PhortuneAccountQuery()) 35 17 ->setViewer($user) 36 18 ->withIDs(array($this->accountID)) ··· 39 21 return new Aphront404Response(); 40 22 } 41 23 24 + $cancel_uri = $this->getApplicationURI($account->getID().'/'); 42 25 $account_uri = $this->getApplicationURI($account->getID().'/'); 43 26 44 - $e_card_number = true; 45 - $e_card_cvc = true; 46 - $e_card_exp = true; 27 + $providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod(); 28 + if (!$providers) { 29 + throw new Exception( 30 + "There are no payment providers enabled that can add payment ". 31 + "methods."); 32 + } 47 33 48 - $errors = array(); 49 - if ($request->isFormPost()) { 50 - $card_errors = $request->getStr('cardErrors'); 51 - $stripe_token = $request->getStr('stripeToken'); 52 - if ($card_errors) { 53 - $raw_errors = json_decode($card_errors); 54 - list($e_card_number, 55 - $e_card_cvc, 56 - $e_card_exp, 57 - $messages) = $this->parseRawErrors($raw_errors); 58 - $errors = array_merge($errors, $messages); 59 - } else if (!$stripe_token) { 60 - $errors[] = pht('There was an unknown error processing your card.'); 34 + $provider_key = $request->getStr('providerKey'); 35 + if (empty($providers[$provider_key])) { 36 + $choices = array(); 37 + foreach ($providers as $provider) { 38 + $choices[] = $this->renderSelectProvider($provider); 61 39 } 40 + return $this->buildResponse($choices, $account_uri); 41 + } 62 42 63 - if (!$errors) { 64 - $root = dirname(phutil_get_library_root('phabricator')); 65 - require_once $root.'/externals/stripe-php/lib/Stripe.php'; 43 + $provider = $providers[$provider_key]; 66 44 67 - try { 68 - // First, make sure the token is valid. 69 - $info = id(new Stripe_Token()) 70 - ->retrieve($stripe_token, $stripe_secret_key); 45 + $errors = array(); 46 + if ($request->isFormPost() && $request->getBool('isProviderForm')) { 47 + $method = id(new PhortunePaymentMethod()) 48 + ->setAccountPHID($account->getPHID()) 49 + ->setAuthorPHID($user->getPHID()) 50 + ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE) 51 + ->setMetadataValue('providerKey', $provider->getProviderKey()); 71 52 72 - // Then, we need to create a Customer in order to be able to charge 73 - // the card more than once. We create one Customer for each card; 74 - // they do not map to PhortuneAccounts because we allow an account to 75 - // have more than one active card. 76 - $customer = Stripe_Customer::create( 77 - array( 78 - 'card' => $stripe_token, 79 - 'description' => $account->getPHID().':'.$user->getUserName(), 80 - ), $stripe_secret_key); 81 - 82 - $card = $info->card; 83 - } catch (Exception $ex) { 84 - phlog($ex); 85 - $errors[] = pht( 86 - 'There was an error communicating with the payments backend.'); 87 - } 53 + $errors = $provider->createPaymentMethodFromRequest($request, $method); 88 54 89 - if (!$errors) { 90 - $payment_method = id(new PhortunePaymentMethod()) 91 - ->setAccountPHID($account->getPHID()) 92 - ->setAuthorPHID($user->getPHID()) 93 - ->setName($card->type.' / '.$card->last4) 94 - ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE) 95 - ->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month)) 96 - ->setMetadata( 97 - array( 98 - 'type' => 'stripe.customer', 99 - 'stripe.customerID' => $customer->id, 100 - 'stripe.tokenID' => $stripe_token, 101 - )) 102 - ->save(); 55 + if (!$errors) { 56 + $method->save(); 103 57 104 - $save_uri = new PhutilURI($account_uri); 105 - $save_uri->setFragment('payment'); 58 + $save_uri = new PhutilURI($account_uri); 59 + $save_uri->setFragment('payment'); 60 + return id(new AphrontRedirectResponse())->setURI($save_uri); 61 + } else { 62 + $dialog = id(new AphrontDialogView()) 63 + ->setUser($user) 64 + ->setTitle(pht('Error Adding Payment Method')) 65 + ->appendChild(id(new AphrontErrorView())->setErrors($errors)) 66 + ->addCancelButton($request->getRequestURI()); 106 67 107 - return id(new AphrontRedirectResponse())->setURI($save_uri); 108 - } 68 + return id(new AphrontDialogResponse())->setDialog($dialog); 109 69 } 70 + } 110 71 111 - $dialog = id(new AphrontDialogView()) 112 - ->setUser($user) 113 - ->setTitle(pht('Error Adding Card')) 114 - ->appendChild(id(new AphrontErrorView())->setErrors($errors)) 115 - ->addCancelButton($request->getRequestURI()); 72 + $form = $provider->renderCreatePaymentMethodForm($request, $errors); 116 73 117 - return id(new AphrontDialogResponse())->setDialog($dialog); 118 - } 74 + $form 75 + ->setUser($user) 76 + ->setAction($request->getRequestURI()) 77 + ->setWorkflow(true) 78 + ->addHiddenInput('providerKey', $provider_key) 79 + ->addHiddenInput('isProviderForm', true) 80 + ->appendChild( 81 + id(new AphrontFormSubmitControl()) 82 + ->setValue(pht('Add Payment Method')) 83 + ->addCancelButton($account_uri)); 119 84 120 85 if ($errors) { 121 86 $errors = id(new AphrontErrorView()) 122 87 ->setErrors($errors); 123 88 } 124 89 125 - $header = id(new PhabricatorHeaderView()) 126 - ->setHeader(pht('Add New Payment Method')); 90 + return $this->buildResponse( 91 + array($errors, $form), 92 + $account_uri); 93 + } 94 + 95 + private function renderSelectProvider( 96 + PhortunePaymentProvider $provider) { 97 + 98 + $request = $this->getRequest(); 99 + $user = $request->getUser(); 100 + 101 + $description = $provider->getPaymentMethodDescription(); 102 + $icon = $provider->getPaymentMethodIcon(); 103 + $details = $provider->getPaymentMethodProviderDescription(); 127 104 128 - $form_id = celerity_generate_unique_node_id(); 129 - require_celerity_resource('stripe-payment-form-css'); 130 - require_celerity_resource('aphront-tooltip-css'); 131 - Javelin::initBehavior('phabricator-tooltips'); 105 + $button = phutil_tag( 106 + 'button', 107 + array( 108 + 'class' => 'grey', 109 + ), 110 + array( 111 + $description, 112 + phutil_tag('br'), 113 + $icon, 114 + $details, 115 + )); 132 116 133 117 $form = id(new AphrontFormView()) 134 - ->setID($form_id) 135 118 ->setUser($user) 136 - ->setWorkflow(true) 137 - ->setAction($request->getRequestURI()) 138 - ->appendChild( 139 - id(new AphrontFormMarkupControl()) 140 - ->setLabel('') 141 - ->setValue( 142 - javelin_tag( 143 - 'div', 144 - array( 145 - 'class' => 'credit-card-logos', 146 - 'sigil' => 'has-tooltip', 147 - 'meta' => array( 148 - 'tip' => 'We support Visa, Mastercard, American Express, '. 149 - 'Discover, JCB, and Diners Club.', 150 - 'size' => 440, 151 - ) 152 - )))) 153 - ->appendChild( 154 - id(new AphrontFormTextControl()) 155 - ->setLabel('Card Number') 156 - ->setDisableAutocomplete(true) 157 - ->setSigil('number-input') 158 - ->setError($e_card_number)) 159 - ->appendChild( 160 - id(new AphrontFormTextControl()) 161 - ->setLabel('CVC') 162 - ->setDisableAutocomplete(true) 163 - ->setSigil('cvc-input') 164 - ->setError($e_card_cvc)) 165 - ->appendChild( 166 - id(new PhortuneMonthYearExpiryControl()) 167 - ->setLabel('Expiration') 168 - ->setUser($user) 169 - ->setError($e_card_exp)) 170 - ->appendChild( 171 - javelin_tag( 172 - 'input', 173 - array( 174 - 'hidden' => true, 175 - 'name' => 'stripeToken', 176 - 'sigil' => 'stripe-token-input', 177 - ))) 178 - ->appendChild( 179 - javelin_tag( 180 - 'input', 181 - array( 182 - 'hidden' => true, 183 - 'name' => 'cardErrors', 184 - 'sigil' => 'card-errors-input' 185 - ))) 186 - ->appendChild( 187 - phutil_tag( 188 - 'input', 189 - array( 190 - 'hidden' => true, 191 - 'name' => 'stripeKey', 192 - 'value' => $stripe_publishable_key, 193 - ))) 194 - ->appendChild( 195 - id(new AphrontFormSubmitControl()) 196 - ->setValue('Add Payment Method') 197 - ->addCancelButton($account_uri)); 119 + ->addHiddenInput('providerKey', $provider->getProviderKey()) 120 + ->appendChild($button); 121 + 122 + return $form; 123 + } 198 124 199 - Javelin::initBehavior( 200 - 'stripe-payment-form', 201 - array( 202 - 'stripePublishKey' => $stripe_publishable_key, 203 - 'root' => $form_id, 204 - )); 125 + private function buildResponse($content, $account_uri) { 126 + $request = $this->getRequest(); 205 127 206 128 $title = pht('Add Payment Method'); 129 + $header = id(new PhabricatorHeaderView()) 130 + ->setHeader($title); 207 131 208 132 $crumbs = $this->buildApplicationCrumbs(); 209 133 $crumbs->addCrumb( ··· 215 139 ->setName(pht('Payment Methods')) 216 140 ->setHref($request->getRequestURI())); 217 141 218 - return 219 - $this->buildStandardPageResponse( 220 - array( 221 - $crumbs, 222 - $header, 223 - $errors, 224 - $form, 225 - ), 226 - array( 227 - 'title' => $title, 228 - 'device' => true, 229 - 'dust' => true, 230 - )); 231 - } 232 - 233 - /** 234 - * Stripe JS and calls to Stripe handle all errors with processing this 235 - * form. This function takes the raw errors - in the form of an array 236 - * where each elementt is $type => $message - and figures out what if 237 - * any fields were invalid and pulls the messages into a flat object. 238 - * 239 - * See https://stripe.com/docs/api#errors for more information on possible 240 - * errors. 241 - */ 242 - private function parseRawErrors($errors) { 243 - $card_number_error = null; 244 - $card_cvc_error = null; 245 - $card_expiration_error = null; 246 - $messages = array(); 247 - foreach ($errors as $index => $error) { 248 - $type = key($error); 249 - $msg = reset($error); 250 - $messages[] = $msg; 251 - switch ($type) { 252 - case 'number': 253 - case 'invalid_number': 254 - case 'incorrect_number': 255 - $card_number_error = pht('Invalid'); 256 - break; 257 - case 'cvc': 258 - case 'invalid_cvc': 259 - case 'incorrect_cvc': 260 - $card_cvc_error = pht('Invalid'); 261 - break; 262 - case 'expiry': 263 - case 'invalid_expiry_month': 264 - case 'invalid_expiry_year': 265 - $card_expiration_error = pht('Invalid'); 266 - break; 267 - case 'card_declined': 268 - case 'expired_card': 269 - case 'duplicate_transaction': 270 - case 'processing_error': 271 - // these errors don't map well to field(s) being bad 272 - break; 273 - case 'invalid_amount': 274 - case 'missing': 275 - default: 276 - // these errors only happen if we (not the user) messed up so log it 277 - $error = sprintf( 278 - 'error_type: %s error_message: %s', 279 - $type, 280 - $msg); 281 - $this->logStripeError($error); 282 - break; 283 - } 284 - } 285 - 286 - return array( 287 - $card_number_error, 288 - $card_cvc_error, 289 - $card_expiration_error, 290 - $messages 291 - ); 292 - } 293 - 294 - private function logStripeError($message) { 295 - phlog('STRIPE-ERROR '.$message); 142 + return $this->buildApplicationPage( 143 + array( 144 + $crumbs, 145 + $header, 146 + $content, 147 + ), 148 + array( 149 + 'title' => $title, 150 + 'device' => true, 151 + 'dust' => true, 152 + )); 296 153 } 297 154 298 155 }
+11
src/applications/phortune/exception/PhortuneNotImplementedException.php
··· 1 + <?php 2 + 3 + final class PhortuneNotImplementedException extends Exception { 4 + 5 + public function __construct(PhortunePaymentProvider $provider) { 6 + $class = get_class($provider); 7 + return parent::__construct( 8 + "Provider '{$provider}' does not implement this method."); 9 + } 10 + 11 + }
+97
src/applications/phortune/provider/PhortunePaymentProvider.php
··· 1 1 <?php 2 2 3 + /** 4 + * @task addmethod Adding Payment Methods 5 + */ 3 6 abstract class PhortunePaymentProvider { 7 + 8 + 9 + /* -( Selecting Providers )------------------------------------------------ */ 10 + 11 + 12 + public static function getAllProviders() { 13 + $objects = id(new PhutilSymbolLoader()) 14 + ->setAncestorClass('PhortunePaymentProvider') 15 + ->loadObjects(); 16 + 17 + return mpull($objects, null, 'getProviderKey'); 18 + } 19 + 20 + public static function getEnabledProviders() { 21 + $providers = self::getAllProviders(); 22 + foreach ($providers as $key => $provider) { 23 + if (!$provider->isEnabled()) { 24 + unset($providers[$key]); 25 + } 26 + } 27 + return $providers; 28 + } 29 + 30 + public static function getProvidersForAddPaymentMethod() { 31 + $providers = self::getEnabledProviders(); 32 + foreach ($providers as $key => $provider) { 33 + if (!$provider->canCreatePaymentMethods()) { 34 + unset($providers[$key]); 35 + } 36 + } 37 + return $providers; 38 + } 39 + 40 + abstract public function isEnabled(); 41 + 42 + final public function getProviderKey() { 43 + return $this->getProviderType().'@'.$this->getProviderDomain(); 44 + } 45 + 46 + 47 + /** 48 + * Return a short string which uniquely identifies this provider's protocol 49 + * type, like "stripe", "paypal", or "balanced". 50 + */ 51 + abstract public function getProviderType(); 52 + 53 + 54 + /** 55 + * Return a short string which uniquely identifies the domain for this 56 + * provider, like "stripe.com" or "google.com". 57 + * 58 + * This is distinct from the provider type so that protocols are not bound 59 + * to a single domain. This is probably not relevant for payments, but this 60 + * assumption burned us pretty hard with authentication and it's easy enough 61 + * to avoid. 62 + */ 63 + abstract public function getProviderDomain(); 64 + 65 + abstract public function getPaymentMethodDescription(); 66 + abstract public function getPaymentMethodIcon(); 67 + abstract public function getPaymentMethodProviderDescription(); 68 + 4 69 5 70 /** 6 71 * Determine of a provider can handle a payment method. ··· 14 79 abstract protected function executeCharge( 15 80 PhortunePaymentMethod $payment_method, 16 81 PhortuneCharge $charge); 82 + 83 + 84 + 85 + /* -( Adding Payment Methods )--------------------------------------------- */ 86 + 87 + 88 + /** 89 + * @task addmethod 90 + */ 91 + public function canCreatePaymentMethods() { 92 + return false; 93 + } 94 + 95 + 96 + /** 97 + * @task addmethod 98 + */ 99 + public function createPaymentMethodFromRequest( 100 + AphrontRequest $request, 101 + PhortunePaymentMethod $method) { 102 + throw new PhortuneNotImplementedException($this); 103 + } 104 + 105 + 106 + /** 107 + * @task addmethod 108 + */ 109 + public function renderCreatePaymentMethodForm( 110 + AphrontRequest $request, 111 + array $errors) { 112 + throw new PhortuneNotImplementedException($this); 113 + } 17 114 18 115 }
+237
src/applications/phortune/provider/PhortuneStripePaymentProvider.php
··· 2 2 3 3 final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { 4 4 5 + public function isEnabled() { 6 + return $this->getPublishableKey() && 7 + $this->getSecretKey(); 8 + } 9 + 10 + public function getProviderType() { 11 + return 'stripe'; 12 + } 13 + 14 + public function getProviderDomain() { 15 + return 'stripe.com'; 16 + } 17 + 18 + public function getPaymentMethodDescription() { 19 + return pht('Add Credit or Debit Card (US and Canada)'); 20 + } 21 + 22 + public function getPaymentMethodIcon() { 23 + return celerity_get_resource_uri('/rsrc/image/phortune/stripe.png'); 24 + } 25 + 26 + public function getPaymentMethodProviderDescription() { 27 + return pht('Processed by Stripe'); 28 + } 29 + 30 + 5 31 public function canHandlePaymentMethod(PhortunePaymentMethod $method) { 6 32 $type = $method->getMetadataValue('type'); 7 33 return ($type === 'stripe.customer'); ··· 30 56 } 31 57 32 58 $charge->setMetadataValue('stripe.chargeID', $id); 59 + } 60 + 61 + private function getPublishableKey() { 62 + return PhabricatorEnv::getEnvConfig('stripe.publishable-key'); 33 63 } 34 64 35 65 private function getSecretKey() { 36 66 return PhabricatorEnv::getEnvConfig('stripe.secret-key'); 67 + } 68 + 69 + 70 + /* -( Adding Payment Methods )--------------------------------------------- */ 71 + 72 + 73 + public function canCreatePaymentMethods() { 74 + return true; 75 + } 76 + 77 + 78 + /** 79 + * @phutil-external-symbol class Stripe_Token 80 + * @phutil-external-symbol class Stripe_Customer 81 + */ 82 + public function createPaymentMethodFromRequest( 83 + AphrontRequest $request, 84 + PhortunePaymentMethod $method) { 85 + 86 + $card_errors = $request->getStr('cardErrors'); 87 + $stripe_token = $request->getStr('stripeToken'); 88 + if ($card_errors) { 89 + $raw_errors = json_decode($card_errors); 90 + $errors = $this->parseRawCreatePaymentMethodErrors($raw_errors); 91 + } else if (!$stripe_token) { 92 + $errors[] = pht('There was an unknown error processing your card.'); 93 + } 94 + 95 + $secret_key = $this->getSecretKey(); 96 + 97 + if (!$errors) { 98 + $root = dirname(phutil_get_library_root('phabricator')); 99 + require_once $root.'/externals/stripe-php/lib/Stripe.php'; 100 + 101 + try { 102 + // First, make sure the token is valid. 103 + $info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key); 104 + 105 + $account_phid = $method->getAccountPHID(); 106 + $author_phid = $method->getAuthorPHID(); 107 + 108 + $params = array( 109 + 'card' => $stripe_token, 110 + 'description' => $account_phid.':'.$author_phid, 111 + ); 112 + 113 + // Then, we need to create a Customer in order to be able to charge 114 + // the card more than once. We create one Customer for each card; 115 + // they do not map to PhortuneAccounts because we allow an account to 116 + // have more than one active card. 117 + $customer = Stripe_Customer::create($params, $secret_key); 118 + 119 + $card = $info->card; 120 + $method 121 + ->setName($card->type.' / '.$card->last4) 122 + ->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month)) 123 + ->setMetadata( 124 + array( 125 + 'type' => 'stripe.customer', 126 + 'stripe.customerID' => $customer->id, 127 + 'stripe.tokenID' => $stripe_token, 128 + )); 129 + } catch (Exception $ex) { 130 + phlog($ex); 131 + $errors[] = pht( 132 + 'There was an error communicating with the payments backend.'); 133 + } 134 + } 135 + 136 + return $errors; 137 + } 138 + 139 + public function renderCreatePaymentMethodForm( 140 + AphrontRequest $request, 141 + array $errors) { 142 + 143 + $e_card_number = isset($errors['number']) ? pht('Invalid') : true; 144 + $e_card_cvc = isset($errors['cvc']) ? pht('Invalid') : true; 145 + $e_card_exp = isset($errors['exp']) ? pht('Invalid') : null; 146 + 147 + $user = $request->getUser(); 148 + 149 + $form_id = celerity_generate_unique_node_id(); 150 + require_celerity_resource('stripe-payment-form-css'); 151 + require_celerity_resource('aphront-tooltip-css'); 152 + Javelin::initBehavior('phabricator-tooltips'); 153 + 154 + $form = id(new AphrontFormView()) 155 + ->setID($form_id) 156 + ->appendChild( 157 + id(new AphrontFormMarkupControl()) 158 + ->setLabel('') 159 + ->setValue( 160 + javelin_tag( 161 + 'div', 162 + array( 163 + 'class' => 'credit-card-logos', 164 + 'sigil' => 'has-tooltip', 165 + 'meta' => array( 166 + 'tip' => 'We support Visa, Mastercard, American Express, '. 167 + 'Discover, JCB, and Diners Club.', 168 + 'size' => 440, 169 + ) 170 + )))) 171 + ->appendChild( 172 + id(new AphrontFormTextControl()) 173 + ->setLabel('Card Number') 174 + ->setDisableAutocomplete(true) 175 + ->setSigil('number-input') 176 + ->setError($e_card_number)) 177 + ->appendChild( 178 + id(new AphrontFormTextControl()) 179 + ->setLabel('CVC') 180 + ->setDisableAutocomplete(true) 181 + ->setSigil('cvc-input') 182 + ->setError($e_card_cvc)) 183 + ->appendChild( 184 + id(new PhortuneMonthYearExpiryControl()) 185 + ->setLabel('Expiration') 186 + ->setUser($user) 187 + ->setError($e_card_exp)) 188 + ->appendChild( 189 + javelin_tag( 190 + 'input', 191 + array( 192 + 'hidden' => true, 193 + 'name' => 'stripeToken', 194 + 'sigil' => 'stripe-token-input', 195 + ))) 196 + ->appendChild( 197 + javelin_tag( 198 + 'input', 199 + array( 200 + 'hidden' => true, 201 + 'name' => 'cardErrors', 202 + 'sigil' => 'card-errors-input' 203 + ))); 204 + 205 + require_celerity_resource('stripe-core'); 206 + Javelin::initBehavior( 207 + 'stripe-payment-form', 208 + array( 209 + 'stripePublishKey' => $this->getPublishableKey(), 210 + 'root' => $form_id, 211 + )); 212 + 213 + return $form; 214 + } 215 + 216 + 217 + /** 218 + * Stripe JS and calls to Stripe handle all errors with processing this 219 + * form. This function takes the raw errors - in the form of an array 220 + * where each elementt is $type => $message - and figures out what if 221 + * any fields were invalid and pulls the messages into a flat object. 222 + * 223 + * See https://stripe.com/docs/api#errors for more information on possible 224 + * errors. 225 + */ 226 + private function parseRawCreatePaymentMethodErrors(array $raw_errors) { 227 + $errors = array(); 228 + 229 + foreach ($raw_errors as $type) { 230 + $error_key = null; 231 + $message = pht('A card processing error has occurred.'); 232 + switch ($type) { 233 + case 'number': 234 + case 'invalid_number': 235 + case 'incorrect_number': 236 + $error_key = 'number'; 237 + $message = pht('Invalid or incorrect credit card number.'); 238 + break; 239 + case 'cvc': 240 + case 'invalid_cvc': 241 + case 'incorrect_cvc': 242 + $error_key = 'cvc'; 243 + $message = pht('Card CVC is invalid or incorrect.'); 244 + break; 245 + case 'expiry': 246 + case 'invalid_expiry_month': 247 + case 'invalid_expiry_year': 248 + $error_key = 'exp'; 249 + $message = pht('Card expiration date is invalid or incorrect.'); 250 + break; 251 + case 'card_declined': 252 + case 'expired_card': 253 + case 'duplicate_transaction': 254 + case 'processing_error': 255 + // these errors don't map well to field(s) being bad 256 + break; 257 + case 'invalid_amount': 258 + case 'missing': 259 + default: 260 + // these errors only happen if we (not the user) messed up so log it 261 + $error = sprintf('[Stripe Error] %s', $type); 262 + phlog($error); 263 + break; 264 + } 265 + 266 + if ($error_key === null || isset($errors[$error_key])) { 267 + $errors[] = $message; 268 + } else { 269 + $errors[$error_key] = $message; 270 + } 271 + } 272 + 273 + return $errors; 37 274 } 38 275 39 276 }
+24
src/applications/phortune/provider/PhortuneTestPaymentProvider.php
··· 2 2 3 3 final class PhortuneTestPaymentProvider extends PhortunePaymentProvider { 4 4 5 + public function isEnabled() { 6 + return true; 7 + } 8 + 9 + public function getProviderType() { 10 + return 'test'; 11 + } 12 + 13 + public function getProviderDomain() { 14 + return 'example.com'; 15 + } 16 + 17 + public function getPaymentMethodDescription() { 18 + return pht('Add Mountain of Virtual Wealth'); 19 + } 20 + 21 + public function getPaymentMethodIcon() { 22 + return celerity_get_resource_uri('/rsrc/image/phortune/test.png'); 23 + } 24 + 25 + public function getPaymentMethodProviderDescription() { 26 + return pht('Infinite Free Money'); 27 + } 28 + 5 29 public function canHandlePaymentMethod(PhortunePaymentMethod $method) { 6 30 $type = $method->getMetadataValue('type'); 7 31 return ($type === 'test.cash' || $type === 'test.multiple');
+24
src/applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php
··· 2 2 3 3 final class PhortuneTestExtraPaymentProvider extends PhortunePaymentProvider { 4 4 5 + public function isEnabled() { 6 + return false; 7 + } 8 + 9 + public function getProviderType() { 10 + return 'test2'; 11 + } 12 + 13 + public function getProviderDomain() { 14 + return 'example.com'; 15 + } 16 + 17 + public function getPaymentMethodDescription() { 18 + return pht('You Should Not Be Able to See This'); 19 + } 20 + 21 + public function getPaymentMethodIcon() { 22 + return celerity_get_resource_uri('/rsrc/image/phortune/test.png'); 23 + } 24 + 25 + public function getPaymentMethodProviderDescription() { 26 + return pht('Just for Unit Tests'); 27 + } 28 + 5 29 public function canHandlePaymentMethod(PhortunePaymentMethod $method) { 6 30 $type = $method->getMetadataValue('type'); 7 31 return ($type === 'test.multiple');
+3 -7
src/applications/phortune/storage/PhortunePaymentMethod.php
··· 60 60 } 61 61 62 62 public function buildPaymentProvider() { 63 - $providers = id(new PhutilSymbolLoader()) 64 - ->setAncestorClass('PhortunePaymentProvider') 65 - ->setConcreteOnly(true) 66 - ->selectAndLoadSymbols(); 63 + $providers = PhortunePaymentProvider::getAllProviders(); 67 64 68 65 $accept = array(); 69 66 foreach ($providers as $provider) { 70 - $obj = newv($provider['name'], array()); 71 - if ($obj->canHandlePaymentMethod($this)) { 72 - $accept[] = $obj; 67 + if ($provider->canHandlePaymentMethod($this)) { 68 + $accept[] = $provider; 73 69 } 74 70 } 75 71
+1 -3
webroot/index.php
··· 20 20 )); 21 21 22 22 DarkConsoleXHProfPluginAPI::hookProfiler(); 23 - 24 - PhutilErrorHandler::setErrorListener( 25 - array('DarkConsoleErrorLogPluginAPI', 'handleErrors')); 23 + DarkConsoleErrorLogPluginAPI::registerErrorHandler(); 26 24 27 25 $sink = new AphrontPHPHTTPSink(); 28 26
+9 -46
webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js
··· 4 4 * javelin-dom 5 5 * javelin-json 6 6 * javelin-workflow 7 - * stripe-core 8 7 */ 9 8 10 9 JX.behavior('stripe-payment-form', function(config) { ··· 23 22 }; 24 23 } 25 24 26 - var stripeErrorObject = function(type) { 27 - var errorPre = 'Stripe (our payments provider) has detected your card '; 28 - var errorPost = ' is invalid.'; 29 - var msg = ''; 30 - var result = {}; 31 - 32 - switch (type) { 33 - case 'number': 34 - msg = errorPre + 'number' + errorPost; 35 - break; 36 - case 'cvc': 37 - msg = errorPre + 'CVC' + errorPost; 38 - break; 39 - case 'expiry': 40 - msg = errorPre + 'expiration date' + errorPost; 41 - break; 42 - case 'stripe': 43 - msg = 'Stripe (our payments provider) is experiencing issues. ' + 44 - 'Please try again.'; 45 - break; 46 - case 'invalid_request': 47 - default: 48 - msg = 'Unknown error.'; 49 - // TODO - how best report bugs? would be good to get 50 - // user feedback since this shouldn't happen! 51 - break; 52 - } 53 - 54 - result[type] = msg; 55 - return result; 56 - } 57 - 58 25 var onsubmit = function(e) { 59 26 e.kill(); 60 27 ··· 63 30 var cardData = getCardData(); 64 31 var errors = []; 65 32 if (!Stripe.validateCardNumber(cardData.number)) { 66 - errors.push(stripeErrorObject('number')); 33 + errors.push('number'); 67 34 } 68 35 if (!Stripe.validateCVC(cardData.cvc)) { 69 - errors.push(stripeErrorObject('cvc')); 36 + errors.push('cvc'); 70 37 } 71 - if (!Stripe.validateExpiry(cardData.month, 72 - cardData.year)) { 73 - errors.push(stripeErrorObject('expiry')); 38 + if (!Stripe.validateExpiry(cardData.month, cardData.year)) { 39 + errors.push('expiry'); 74 40 } 75 - if (errors.length != 0) { 41 + 42 + if (errors.length) { 76 43 cardErrors.value = JX.JSON.stringify(errors); 77 - 78 44 JX.Workflow.newFromForm(root) 79 45 .start(); 80 - 81 46 return; 82 47 } 83 48 ··· 97 62 var errors = []; 98 63 switch (response.error.type) { 99 64 case 'card_error': 100 - var error = {}; 101 - error[response.error.code] = response.error.message; 102 - errors.push(error); 65 + errors.push(response.error.code); 103 66 break; 104 67 case 'invalid_request_error': 105 - errors.push(stripeErrorObject('invalid_request')); 68 + errors.push('invalid_request'); 106 69 break; 107 70 case 'api_error': 108 71 default: 109 - errors.push(stripeErrorObject('stripe')); 72 + errors.push('stripe'); 110 73 break; 111 74 } 112 75 cardErrors.value = JX.JSON.stringify(errors);