@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.

Improve UI/UX when users try to add an invalid card with Stripe

Summary: Ref T13244. See PHI1052. Our error handling for Stripe errors isn't great right now. We can give users a bit more information, and a less jarring UI.

Test Plan:
Before (this is in developer mode, production doesn't get a stack trace):

{F6197394}

After:

{F6197397}

- Tried all the invalid test codes listed here: https://stripe.com/docs/testing#cards

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13244

Differential Revision: https://secure.phabricator.com/D20132

+148 -29
+3 -3
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => '3c8a0668', 11 11 'conpherence.pkg.js' => '020aebcf', 12 - 'core.pkg.css' => 'eab5ccaf', 12 + 'core.pkg.css' => '08baca0c', 13 13 'core.pkg.js' => '5c737607', 14 14 'differential.pkg.css' => 'b8df73d4', 15 15 'differential.pkg.js' => '67c9ea4c', ··· 30 30 'rsrc/css/aphront/notification.css' => '30240bd2', 31 31 'rsrc/css/aphront/panel-view.css' => '46923d46', 32 32 'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf', 33 - 'rsrc/css/aphront/table-view.css' => '76eda3f8', 33 + 'rsrc/css/aphront/table-view.css' => 'daa1f9df', 34 34 'rsrc/css/aphront/tokenizer.css' => 'b52d0668', 35 35 'rsrc/css/aphront/tooltip.css' => 'e3f2412f', 36 36 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', ··· 519 519 'aphront-list-filter-view-css' => 'feb64255', 520 520 'aphront-multi-column-view-css' => 'fbc00ba3', 521 521 'aphront-panel-view-css' => '46923d46', 522 - 'aphront-table-view-css' => '76eda3f8', 522 + 'aphront-table-view-css' => 'daa1f9df', 523 523 'aphront-tokenizer-control-css' => 'b52d0668', 524 524 'aphront-tooltip-css' => 'e3f2412f', 525 525 'aphront-typeahead-control-css' => '8779483d',
+2
src/__phutil_library_map__.php
··· 5015 5015 'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php', 5016 5016 'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php', 5017 5017 'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php', 5018 + 'PhortuneDisplayException' => 'applications/phortune/exception/PhortuneDisplayException.php', 5018 5019 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', 5019 5020 'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php', 5020 5021 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', ··· 11263 11264 'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer', 11264 11265 'PhortuneCurrencyTestCase' => 'PhabricatorTestCase', 11265 11266 'PhortuneDAO' => 'PhabricatorLiskDAO', 11267 + 'PhortuneDisplayException' => 'Exception', 11266 11268 'PhortuneErrCode' => 'PhortuneConstants', 11267 11269 'PhortuneInvoiceView' => 'AphrontTagView', 11268 11270 'PhortuneLandingController' => 'PhortuneController',
+3
src/applications/fund/storage/FundBacker.php
··· 76 76 public function getCapabilities() { 77 77 return array( 78 78 PhabricatorPolicyCapability::CAN_VIEW, 79 + PhabricatorPolicyCapability::CAN_EDIT, 79 80 ); 80 81 } 81 82 ··· 90 91 if ($initiative) { 91 92 return $initiative->getPolicy($capability); 92 93 } 94 + return PhabricatorPolicies::POLICY_NOONE; 95 + case PhabricatorPolicyCapability::CAN_EDIT: 93 96 return PhabricatorPolicies::POLICY_NOONE; 94 97 } 95 98 }
+24 -10
src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php
··· 73 73 $provider = $providers[$provider_id]; 74 74 75 75 $errors = array(); 76 + $display_exception = null; 76 77 if ($request->isFormPost() && $request->getBool('isProviderForm')) { 77 78 $method = id(new PhortunePaymentMethod()) 78 79 ->setAccountPHID($account->getPHID()) ··· 107 108 } 108 109 109 110 if (!$errors) { 110 - $errors = $provider->createPaymentMethodFromRequest( 111 - $request, 112 - $method, 113 - $client_token); 111 + try { 112 + $provider->createPaymentMethodFromRequest( 113 + $request, 114 + $method, 115 + $client_token); 116 + } catch (PhortuneDisplayException $exception) { 117 + $display_exception = $exception; 118 + } catch (Exception $ex) { 119 + $errors = array( 120 + pht('There was an error adding this payment method:'), 121 + $ex->getMessage(), 122 + ); 123 + } 114 124 } 115 125 } 116 126 117 - if (!$errors) { 127 + if (!$errors && !$display_exception) { 118 128 $method->save(); 119 129 120 130 // If we added this method on a cart flow, return to the cart to ··· 133 143 134 144 return id(new AphrontRedirectResponse())->setURI($next_uri); 135 145 } else { 136 - $dialog = id(new AphrontDialogView()) 137 - ->setUser($viewer) 146 + if ($display_exception) { 147 + $dialog_body = $display_exception->getView(); 148 + } else { 149 + $dialog_body = id(new PHUIInfoView()) 150 + ->setErrors($errors); 151 + } 152 + 153 + return $this->newDialog() 138 154 ->setTitle(pht('Error Adding Payment Method')) 139 - ->appendChild(id(new PHUIInfoView())->setErrors($errors)) 155 + ->appendChild($dialog_body) 140 156 ->addCancelButton($request->getRequestURI()); 141 - 142 - return id(new AphrontDialogResponse())->setDialog($dialog); 143 157 } 144 158 } 145 159
+15
src/applications/phortune/exception/PhortuneDisplayException.php
··· 1 + <?php 2 + 3 + final class PhortuneDisplayException 4 + extends Exception { 5 + 6 + public function setView($view) { 7 + $this->view = $view; 8 + return $this; 9 + } 10 + 11 + public function getView() { 12 + return $this->view; 13 + } 14 + 15 + }
+89 -5
src/applications/phortune/provider/PhortuneStripePaymentProvider.php
··· 233 233 array $token) { 234 234 $this->loadStripeAPILibraries(); 235 235 236 - $errors = array(); 237 - 238 236 $secret_key = $this->getSecretKey(); 239 237 $stripe_token = $token['stripeCardToken']; 240 238 ··· 253 251 // the card more than once. We create one Customer for each card; 254 252 // they do not map to PhortuneAccounts because we allow an account to 255 253 // have more than one active card. 256 - $customer = Stripe_Customer::create($params, $secret_key); 254 + try { 255 + $customer = Stripe_Customer::create($params, $secret_key); 256 + } catch (Stripe_CardError $ex) { 257 + $display_exception = $this->newDisplayExceptionFromCardError($ex); 258 + if ($display_exception) { 259 + throw $display_exception; 260 + } 261 + throw $ex; 262 + } 257 263 258 264 $card = $info->card; 259 265 ··· 267 273 'stripe.customerID' => $customer->id, 268 274 'stripe.cardToken' => $stripe_token, 269 275 )); 270 - 271 - return $errors; 272 276 } 273 277 274 278 public function renderCreatePaymentMethodForm( ··· 382 386 $root = dirname(phutil_get_library_root('phabricator')); 383 387 require_once $root.'/externals/stripe-php/lib/Stripe.php'; 384 388 } 389 + 390 + 391 + private function newDisplayExceptionFromCardError(Stripe_CardError $ex) { 392 + $body = $ex->getJSONBody(); 393 + if (!$body) { 394 + return null; 395 + } 396 + 397 + $map = idx($body, 'error'); 398 + if (!$map) { 399 + return null; 400 + } 401 + 402 + $view = array(); 403 + 404 + $message = idx($map, 'message'); 405 + 406 + $view[] = id(new PHUIInfoView()) 407 + ->setErrors(array($message)); 408 + 409 + $view[] = phutil_tag( 410 + 'div', 411 + array( 412 + 'class' => 'mlt mlb', 413 + ), 414 + pht('Additional details about this error:')); 415 + 416 + $rows = array(); 417 + 418 + $rows[] = array( 419 + pht('Error Code'), 420 + idx($map, 'code'), 421 + ); 422 + 423 + $rows[] = array( 424 + pht('Error Type'), 425 + idx($map, 'type'), 426 + ); 427 + 428 + $param = idx($map, 'param'); 429 + if (strlen($param)) { 430 + $rows[] = array( 431 + pht('Error Param'), 432 + $param, 433 + ); 434 + } 435 + 436 + $decline_code = idx($map, 'decline_code'); 437 + if (strlen($decline_code)) { 438 + $rows[] = array( 439 + pht('Decline Code'), 440 + $decline_code, 441 + ); 442 + } 443 + 444 + $doc_url = idx($map, 'doc_url'); 445 + if ($doc_url) { 446 + $rows[] = array( 447 + pht('Learn More'), 448 + phutil_tag( 449 + 'a', 450 + array( 451 + 'href' => $doc_url, 452 + 'target' => '_blank', 453 + ), 454 + $doc_url), 455 + ); 456 + } 457 + 458 + $view[] = id(new AphrontTableView($rows)) 459 + ->setColumnClasses( 460 + array( 461 + 'header', 462 + 'wide', 463 + )); 464 + 465 + return id(new PhortuneDisplayException(get_class($ex))) 466 + ->setView($view); 467 + } 468 + 385 469 386 470 }
+4 -3
src/applications/policy/filter/PhabricatorPolicyFilter.php
··· 175 175 if (!in_array($capability, $object_capabilities)) { 176 176 throw new Exception( 177 177 pht( 178 - "Testing for capability '%s' on an object which does ". 179 - "not have that capability!", 180 - $capability)); 178 + 'Testing for capability "%s" on an object ("%s") which does '. 179 + 'not support that capability.', 180 + $capability, 181 + get_class($object))); 181 182 } 182 183 183 184 $policy = $this->getObjectPolicy($object, $capability);
+8 -8
webroot/rsrc/css/aphront/table-view.css
··· 45 45 background: inherit; 46 46 } 47 47 48 - .aphront-table-view th { 48 + .aphront-table-view th, 49 + .aphront-table-view td.header { 49 50 font-weight: bold; 50 51 white-space: nowrap; 51 52 color: {$bluetext}; 52 - text-shadow: 0 1px 0 white; 53 53 font-weight: bold; 54 - border-bottom: 1px solid {$thinblueborder}; 54 + text-shadow: 0 1px 0 white; 55 55 background-color: {$lightbluebackground}; 56 + } 57 + 58 + .aphront-table-view th { 59 + border-bottom: 1px solid {$thinblueborder}; 56 60 } 57 61 58 62 th.aphront-table-view-sortable-selected { ··· 74 78 } 75 79 76 80 .aphront-table-view td.header { 77 - padding: 4px 8px; 78 - white-space: nowrap; 79 81 text-align: right; 80 - color: {$bluetext}; 81 - font-weight: bold; 82 - vertical-align: top; 82 + border-right: 1px solid {$thinblueborder}; 83 83 } 84 84 85 85 .aphront-table-view td {