From 4f3b5f0ea97491e610b7cb4c84cc56cb5f9c19a2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Mar 2013 09:11:42 -0700 Subject: [PATCH] Phortune v0.1: add payment methods Summary: Hook @btrahan's Stripe form to the rest of Phortune. - Users can add payment methods. - They are saved to Stripe and associated with PhortunePaymentMethods on our side. - Payment methods appear on account overview. Test Plan: {F37548} {F37549} {F37550} Reviewers: chad, btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T2787 Differential Revision: https://secure.phabricator.com/D5438 --- .../sql/patches/20130323.phortunepayment.sql | 14 + src/__phutil_library_map__.php | 12 +- .../PhabricatorApplicationPhortune.php | 9 +- .../PhortuneAccountViewController.php | 34 ++ .../PhortunePaymentMethodEditController.php | 298 ++++++++++++++++++ .../option/PhabricatorStripeConfigOptions.php | 25 ++ .../query/PhortunePaymentMethodQuery.php | 117 +++++++ .../storage/PhortunePaymentMethod.php | 6 + .../PhortuneStripeBaseController.php | 17 - ...hortuneStripeTestPaymentFormController.php | 146 --------- .../view/PhortuneStripePaymentFormView.php | 4 +- .../patch/PhabricatorBuiltinPatchList.php | 4 + .../phortune/behavior-stripe-payment-form.js | 19 +- 13 files changed, 521 insertions(+), 184 deletions(-) create mode 100644 resources/sql/patches/20130323.phortunepayment.sql create mode 100644 src/applications/phortune/controller/PhortunePaymentMethodEditController.php create mode 100644 src/applications/phortune/option/PhabricatorStripeConfigOptions.php create mode 100644 src/applications/phortune/query/PhortunePaymentMethodQuery.php delete mode 100644 src/applications/phortune/stripe/controller/PhortuneStripeBaseController.php delete mode 100644 src/applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php rename src/applications/phortune/{stripe => }/view/PhortuneStripePaymentFormView.php (97%) diff --git a/resources/sql/patches/20130323.phortunepayment.sql b/resources/sql/patches/20130323.phortunepayment.sql new file mode 100644 index 0000000000..fcdca6fa62 --- /dev/null +++ b/resources/sql/patches/20130323.phortunepayment.sql @@ -0,0 +1,14 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_paymentmethod ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(255) NOT NULL, + status VARCHAR(64) NOT NULL COLLATE utf8_bin, + accountPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + expiresEpoch INT UNSIGNED, + metadata LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + KEY `key_account` (accountPHID, status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 503dce3f3f..0de4836064 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1359,6 +1359,7 @@ phutil_register_library_map(array( 'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php', 'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php', 'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php', + 'PhabricatorStripeConfigOptions' => 'applications/phortune/option/PhabricatorStripeConfigOptions.php', 'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php', 'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php', 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', @@ -1540,13 +1541,13 @@ phutil_register_library_map(array( 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', 'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php', + 'PhortunePaymentMethodEditController' => 'applications/phortune/controller/PhortunePaymentMethodEditController.php', 'PhortunePaymentMethodListController' => 'applications/phortune/controller/PhortunePaymentMethodListController.php', + 'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php', 'PhortunePaymentMethodViewController' => 'applications/phortune/controller/PhortunePaymentMethodViewController.php', 'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php', 'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php', - 'PhortuneStripeBaseController' => 'applications/phortune/stripe/controller/PhortuneStripeBaseController.php', - 'PhortuneStripePaymentFormView' => 'applications/phortune/stripe/view/PhortuneStripePaymentFormView.php', - 'PhortuneStripeTestPaymentFormController' => 'applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php', + 'PhortuneStripePaymentFormView' => 'applications/phortune/view/PhortuneStripePaymentFormView.php', 'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php', 'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php', 'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php', @@ -2977,6 +2978,7 @@ phutil_register_library_map(array( 'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow', + 'PhabricatorStripeConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSubscribersQuery' => 'PhabricatorQuery', 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', @@ -3194,13 +3196,13 @@ phutil_register_library_map(array( 0 => 'PhortuneDAO', 1 => 'PhabricatorPolicyInterface', ), + 'PhortunePaymentMethodEditController' => 'PhortuneController', 'PhortunePaymentMethodListController' => 'PhabricatorController', + 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortunePaymentMethodViewController' => 'PhabricatorController', 'PhortuneProduct' => 'PhortuneDAO', 'PhortunePurchase' => 'PhortuneDAO', - 'PhortuneStripeBaseController' => 'PhabricatorController', 'PhortuneStripePaymentFormView' => 'AphrontView', - 'PhortuneStripeTestPaymentFormController' => 'PhortuneStripeBaseController', 'PhrictionActionConstants' => 'PhrictionConstants', 'PhrictionChangeType' => 'PhrictionConstants', 'PhrictionContent' => diff --git a/src/applications/phortune/application/PhabricatorApplicationPhortune.php b/src/applications/phortune/application/PhabricatorApplicationPhortune.php index 99d5b9e17a..80c6d7b4a8 100644 --- a/src/applications/phortune/application/PhabricatorApplicationPhortune.php +++ b/src/applications/phortune/application/PhabricatorApplicationPhortune.php @@ -32,17 +32,14 @@ final class PhabricatorApplicationPhortune extends PhabricatorApplication { '' => 'PhortuneLandingController', '(?P\d+)/' => array( '' => 'PhortuneAccountViewController', + 'paymentmethod/' => array( + 'edit/' => 'PhortunePaymentMethodEditController', + ), ), - 'account/' => array( '' => 'PhortuneAccountListController', 'edit/(?:(?P\d+)/)?' => 'PhortuneAccountEditController', ), - 'paymentmethod/' => array( - '' => 'PhortunePaymentMethodListController', - 'view/(?P\d+)/' => 'PhortunePaymentMethodViewController', - 'edit/(?:(?P\d+)/)?' => 'PhortunePaymentMethodEditController', - ), 'stripe/' => array( 'testpaymentform/' => 'PhortuneStripeTestPaymentFormController', ), diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php index 616914937b..b86820c5c2 100644 --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -95,6 +95,40 @@ final class PhortuneAccountViewController extends PhortuneController { ->setNoDataString( pht('No payment methods associated with this account.')); + $methods = id(new PhortunePaymentMethodQuery()) + ->setViewer($user) + ->withAccountPHIDs(array($account->getPHID())) + ->withStatus(PhortunePaymentMethodQuery::STATUS_OPEN) + ->execute(); + + if ($methods) { + $this->loadHandles(mpull($methods, 'getAuthorPHID')); + } + + foreach ($methods as $method) { + $item = new PhabricatorObjectItemView(); + $item->setHeader($method->getName()); + + switch ($method->getStatus()) { + case PhortunePaymentMethod::STATUS_ACTIVE: + $item->addAttribute(pht('Active')); + $item->setBarColor('green'); + break; + } + + $item->addAttribute( + pht( + 'Added %s by %s', + phabricator_datetime($method->getDateCreated(), $user), + $this->getHandle($method->getAuthorPHID())->renderLink())); + + if ($method->getExpiresEpoch() < time() + (60 * 60 * 24 * 30)) { + $item->addAttribute(pht('Expires Soon!')); + } + + $list->addItem($item); + } + return array( $header, $actions, diff --git a/src/applications/phortune/controller/PhortunePaymentMethodEditController.php b/src/applications/phortune/controller/PhortunePaymentMethodEditController.php new file mode 100644 index 0000000000..7d618d73b1 --- /dev/null +++ b/src/applications/phortune/controller/PhortunePaymentMethodEditController.php @@ -0,0 +1,298 @@ +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)) + ->executeOne(); + if (!$account) { + return new Aphront404Response(); + } + + $account_uri = $this->getApplicationURI($account->getID().'/'); + + $e_card_number = true; + $e_card_cvc = true; + $e_card_exp = true; + + $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.'); + } + + 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()) + ->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', + 'stripeCustomerID' => $customer->id, + 'stripeTokenID' => $stripe_token, + )) + ->save(); + + $save_uri = new PhutilURI($account_uri); + $save_uri->setFragment('payment'); + + return id(new AphrontRedirectResponse())->setURI($save_uri); + } + } + + $dialog = id(new AphrontDialogView()) + ->setUser($user) + ->setTitle(pht('Error Adding Card')) + ->appendChild(id(new AphrontErrorView())->setErrors($errors)) + ->addCancelButton($request->getRequestURI()); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + if ($errors) { + $errors = id(new AphrontErrorView()) + ->setErrors($errors); + } + + $header = id(new PhabricatorHeaderView()) + ->setHeader(pht('Add New Payment Method')); + + $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) + ->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( + 'stripe-payment-form', + array( + 'stripePublishKey' => $stripe_publishable_key, + 'root' => $form_id, + )); + + $title = pht('Add Payment Method'); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('Account')) + ->setHref($account_uri)); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->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); + } + +} diff --git a/src/applications/phortune/option/PhabricatorStripeConfigOptions.php b/src/applications/phortune/option/PhabricatorStripeConfigOptions.php new file mode 100644 index 0000000000..ab1dd39087 --- /dev/null +++ b/src/applications/phortune/option/PhabricatorStripeConfigOptions.php @@ -0,0 +1,25 @@ +newOption('stripe.publishable-key', 'string', null) + ->setDescription( + pht('Stripe publishable key.')), + $this->newOption('stripe.secret-key', 'string', null) + ->setDescription( + pht('Stripe secret key.')), + ); + } + +} diff --git a/src/applications/phortune/query/PhortunePaymentMethodQuery.php b/src/applications/phortune/query/PhortunePaymentMethodQuery.php new file mode 100644 index 0000000000..a950d1a4b6 --- /dev/null +++ b/src/applications/phortune/query/PhortunePaymentMethodQuery.php @@ -0,0 +1,117 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withAccountPHIDs(array $phids) { + $this->accountPHIDs = $phids; + return $this; + } + + public function withStatus($status) { + $this->status = $status; + return $this; + } + + protected function loadPage() { + $table = new PhortunePaymentMethod(); + $conn = $table->establishConnection('r'); + + $rows = queryfx_all( + $conn, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn), + $this->buildOrderClause($conn), + $this->buildLimitClause($conn)); + + return $table->loadAllFromArray($rows); + } + + protected function willFilterPage(array $methods) { + if (!$methods) { + return array(); + } + + $accounts = id(new PhortuneAccountQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(mpull($methods, 'getAccountPHID')) + ->execute(); + $accounts = mpull($accounts, null, 'getPHID'); + + foreach ($methods as $key => $method) { + $account = idx($accounts, $method->getAccountPHID()); + if (!$account) { + unset($methods[$key]); + continue; + } + $method->attachAccount($account); + } + + return $methods; + } + + private function buildWhereClause(AphrontDatabaseConnection $conn) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->accountPHIDs) { + $where[] = qsprintf( + $conn, + 'accountPHID IN (%Ls)', + $this->accountPHIDs); + } + + switch ($this->status) { + case self::STATUS_ANY; + break; + case self::STATUS_OPEN: + $where[] = qsprintf( + $conn, + 'status in (%Ls)', + array( + PhortunePaymentMethod::STATUS_ACTIVE, + PhortunePaymentMethod::STATUS_FAILED, + )); + break; + default: + throw new Exception("Unknown status '{$this->status}'!"); + } + + $where[] = $this->buildPagingClause($conn); + + return $this->formatWhereClause($where); + } + +} diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php index dfa0465e64..4aa0b11d03 100644 --- a/src/applications/phortune/storage/PhortunePaymentMethod.php +++ b/src/applications/phortune/storage/PhortunePaymentMethod.php @@ -7,9 +7,15 @@ final class PhortunePaymentMethod extends PhortuneDAO implements PhabricatorPolicyInterface { + const STATUS_ACTIVE = 'payment:active'; + const STATUS_FAILED = 'payment:failed'; + const STATUS_REMOVED = 'payment:removed'; + protected $name; + protected $status; protected $accountPHID; protected $authorPHID; + protected $expiresEpoch; protected $metadata; private $account; diff --git a/src/applications/phortune/stripe/controller/PhortuneStripeBaseController.php b/src/applications/phortune/stripe/controller/PhortuneStripeBaseController.php deleted file mode 100644 index f98c4087b1..0000000000 --- a/src/applications/phortune/stripe/controller/PhortuneStripeBaseController.php +++ /dev/null @@ -1,17 +0,0 @@ -buildStandardPageView(); - - $page->setApplicationName('Phortune - Stripe'); - $page->setBaseURI('/phortune/stripe/'); - $page->setTitle(idx($data, 'title')); - $page->appendChild($view); - - $response = new AphrontWebpageResponse(); - return $response->setContent($page->render()); - } - -} diff --git a/src/applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php b/src/applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php deleted file mode 100644 index e64d39b256..0000000000 --- a/src/applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php +++ /dev/null @@ -1,146 +0,0 @@ -getRequest(); - $user = $request->getUser(); - $title = 'Test Payment Form'; - $error_view = null; - $card_number_error = null; - $card_cvc_error = null; - $card_expiration_error = null; - $stripe_key = $request->getStr('stripeKey'); - if (!$stripe_key) { - $error_view = id(new AphrontErrorView()) - ->setTitle('Missing stripeKey parameter in URI'); - } - - if (!$error_view && $request->isFormPost()) { - $card_errors = $request->getStr('cardErrors'); - $stripe_token = $request->getStr('stripeToken'); - if ($card_errors) { - $raw_errors = json_decode($card_errors); - list($card_number_error, - $card_cvc_error, - $card_expiration_error, - $messages) = $this->parseRawErrors($raw_errors); - $error_view = id(new AphrontErrorView()) - ->setTitle('There were errors processing your card.') - ->setErrors($messages); - } else if (!$stripe_token) { - // this shouldn't happen, so show the user a very generic error - // message and log that this error occurred...! - $error_view = id(new AphrontErrorView()) - ->setTitle('There was an unknown error processing your card.') - ->setErrors(array('Please try again.')); - $error = 'payment form submitted but no stripe token and no errors'; - $this->logStripeError($error); - } else { - // success -- do something with $stripe_token!! - } - } else if (!$error_view) { - $error_view = id(new AphrontErrorView()) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setTitle( - 'If you are using a test stripe key, use 4242424242424242, '. - 'any three digits for CVC, and any valid expiration date to '. - 'test!'); - } - - $view = id(new AphrontPanelView()) - ->setWidth(AphrontPanelView::WIDTH_FORM) - ->setHeader($title); - - $form = id(new PhortuneStripePaymentFormView()) - ->setUser($user) - ->setStripeKey($stripe_key) - ->setCardNumberError($card_number_error) - ->setCardCVCError($card_cvc_error) - ->setCardExpirationError($card_expiration_error); - - $view->appendChild($form); - - return - $this->buildStandardPageResponse( - array( - $error_view, - $view, - ), - array( - 'title' => $title, - )); - } - - /** - * 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 = true; - break; - case 'cvc': - case 'invalid_cvc': - case 'incorrect_cvc': - $card_cvc_error = true; - break; - case 'expiry': - case 'invalid_expiry_month': - case 'invalid_expiry_year': - $card_expiration_error = true; - 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; - } - } - - // append a helpful "fix this" to the messages to be displayed to the user - $messages[] = pht( - 'Please fix these errors and try again.', - count($messages)); - - return array( - $card_number_error, - $card_cvc_error, - $card_expiration_error, - $messages - ); - } - - private function logStripeError($message) { - phlog('STRIPE-ERROR '.$message); - } - - -} diff --git a/src/applications/phortune/stripe/view/PhortuneStripePaymentFormView.php b/src/applications/phortune/view/PhortuneStripePaymentFormView.php similarity index 97% rename from src/applications/phortune/stripe/view/PhortuneStripePaymentFormView.php rename to src/applications/phortune/view/PhortuneStripePaymentFormView.php index 5d39ac9654..d037894d6c 100644 --- a/src/applications/phortune/stripe/view/PhortuneStripePaymentFormView.php +++ b/src/applications/phortune/view/PhortuneStripePaymentFormView.php @@ -39,7 +39,7 @@ final class PhortuneStripePaymentFormView extends AphrontView { } public function render() { - $form_id = celerity_generate_unique_node_id(); + $form_id = celerity_generate_unique_node_id(); require_celerity_resource('stripe-payment-form-css'); require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-tooltips'); @@ -105,7 +105,7 @@ final class PhortuneStripePaymentFormView extends AphrontView { ))) ->appendChild( id(new AphrontFormSubmitControl()) - ->setValue('Submit Payment')); + ->setValue('Add Payment Method')); Javelin::initBehavior( 'stripe-payment-form', diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index cd0bb98513..04af976eae 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1206,6 +1206,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'sql', 'name' => $this->getPatchPath('20130322.phortune.sql'), ), + '20130323.phortunepayment.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20130323.phortunepayment.sql'), + ), ); } 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 1910242918..23645be1c5 100644 --- a/webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js +++ b/webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js @@ -3,6 +3,7 @@ * @requires javelin-behavior * javelin-dom * javelin-json + * javelin-workflow * stripe-core */ @@ -73,8 +74,11 @@ JX.behavior('stripe-payment-form', function(config) { } if (errors.length != 0) { cardErrors.value = JX.JSON.stringify(errors); - root.submit(); - return true; + + JX.Workflow.newFromForm(root) + .start(); + + return; } // no errors detected so contact Stripe asynchronously @@ -110,14 +114,13 @@ JX.behavior('stripe-payment-form', function(config) { // success - we can use the token to create a customer object with // Stripe and let the billing commence! var token = response['id']; + cardErrors.value = '[]'; stripeToken.value = token; } - root.submit(); + + JX.Workflow.newFromForm(root) + .start(); } - JX.DOM.listen( - root, - 'submit', - null, - onsubmit); + JX.DOM.listen(root, 'submit', null, onsubmit); });