diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index ccaa18d5ac..a1c0f375ff 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1274,6 +1274,20 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/diffusion/behavior-audit-preview.js', ), + 'javelin-behavior-balanced-payment-form' => + array( + 'uri' => '/res/2a850a31/rsrc/js/application/phortune/behavior-balanced-payment-form.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-json', + 3 => 'javelin-workflow', + 4 => 'phortune-credit-card-form', + ), + 'disk' => '/rsrc/js/application/phortune/behavior-balanced-payment-form.js', + ), 'javelin-behavior-conpherence-drag-and-drop-photo' => array( 'uri' => '/res/9e3eb1cd/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js', @@ -2258,7 +2272,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-stripe-payment-form' => array( - 'uri' => '/res/62dc91b4/rsrc/js/application/phortune/behavior-stripe-payment-form.js', + 'uri' => '/res/2ae12d96/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 'type' => 'js', 'requires' => array( diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 31034c0d48..7e01dbb608 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1219,6 +1219,7 @@ phutil_register_library_map(array( 'PhabricatorPhabricatorOAuthConfigOptions' => 'applications/config/option/PhabricatorPhabricatorOAuthConfigOptions.php', 'PhabricatorPhameConfigOptions' => 'applications/phame/config/PhabricatorPhameConfigOptions.php', 'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php', + 'PhabricatorPhortuneConfigOptions' => 'applications/phortune/option/PhabricatorPhortuneConfigOptions.php', 'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php', 'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php', 'PhabricatorPinboardItemView' => 'view/layout/PhabricatorPinboardItemView.php', @@ -1400,7 +1401,6 @@ 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', @@ -1580,6 +1580,7 @@ phutil_register_library_map(array( 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', 'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php', 'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php', + 'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php', 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 'PhortuneController' => 'applications/phortune/controller/PhortuneController.php', @@ -2923,6 +2924,7 @@ phutil_register_library_map(array( 'PhabricatorPhabricatorOAuthConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhameConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorPhortuneConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPinboardItemView' => 'AphrontView', @@ -3091,7 +3093,6 @@ phutil_register_library_map(array( 'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow', - 'PhabricatorStripeConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSubscribersQuery' => 'PhabricatorQuery', 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', @@ -3302,6 +3303,7 @@ phutil_register_library_map(array( 'PhortuneAccountTransaction' => 'PhabricatorApplicationTransaction', 'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneAccountViewController' => 'PhortuneController', + 'PhortuneBalancedPaymentProvider' => 'PhortunePaymentProvider', 'PhortuneCart' => 'PhortuneDAO', 'PhortuneCharge' => 'PhortuneDAO', 'PhortuneController' => 'PhabricatorController', diff --git a/src/applications/phortune/exception/PhortuneNotImplementedException.php b/src/applications/phortune/exception/PhortuneNotImplementedException.php index 89a2258b18..7912ab30f2 100644 --- a/src/applications/phortune/exception/PhortuneNotImplementedException.php +++ b/src/applications/phortune/exception/PhortuneNotImplementedException.php @@ -5,7 +5,7 @@ final class PhortuneNotImplementedException extends Exception { public function __construct(PhortunePaymentProvider $provider) { $class = get_class($provider); return parent::__construct( - "Provider '{$provider}' does not implement this method."); + "Provider '{$class}' does not implement this method."); } } diff --git a/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php b/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php new file mode 100644 index 0000000000..67ee8367df --- /dev/null +++ b/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php @@ -0,0 +1,31 @@ +newOption('phortune.stripe.publishable-key', 'string', null) + ->setLocked(true) + ->setDescription(pht('Stripe publishable key.')), + $this->newOption('phortune.stripe.secret-key', 'string', null) + ->setHidden(true) + ->setDescription(pht('Stripe secret key.')), + $this->newOption('phortune.balanced.marketplace-uri', 'string', null) + ->setLocked(true) + ->setDescription(pht('Balanced Marketplace URI.')), + $this->newOption('phortune.balanced.secret-key', 'string', null) + ->setHidden(true) + ->setDescription(pht('Balanced secret key.')), + ); + } + +} diff --git a/src/applications/phortune/option/PhabricatorStripeConfigOptions.php b/src/applications/phortune/option/PhabricatorStripeConfigOptions.php deleted file mode 100644 index ab1dd39087..0000000000 --- a/src/applications/phortune/option/PhabricatorStripeConfigOptions.php +++ /dev/null @@ -1,25 +0,0 @@ -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/provider/PhortuneBalancedPaymentProvider.php b/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php new file mode 100644 index 0000000000..ae18b6aa5e --- /dev/null +++ b/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php @@ -0,0 +1,171 @@ +getMarketplaceURI() && + $this->getSecretKey(); + } + + public function getProviderType() { + return 'balanced'; + } + + public function getProviderDomain() { + return 'balancedpayments.com'; + } + + public function getPaymentMethodDescription() { + return pht('Add Credit or Debit Card'); + } + + public function getPaymentMethodIcon() { + return celerity_get_resource_uri('/rsrc/image/phortune/balanced.png'); + } + + public function getPaymentMethodProviderDescription() { + return pht('Processed by Balanced'); + } + + + public function canHandlePaymentMethod(PhortunePaymentMethod $method) { + $type = $method->getMetadataValue('type'); + return ($type === 'balanced.account'); + } + + protected function executeCharge( + PhortunePaymentMethod $method, + PhortuneCharge $charge) { + throw new PhortuneNotImplementedException($this); + } + + private function getMarketplaceURI() { + return PhabricatorEnv::getEnvConfig('phortune.balanced.marketplace-uri'); + } + + private function getSecretKey() { + return PhabricatorEnv::getEnvConfig('phortune.balanced.secret-key'); + } + + +/* -( Adding Payment Methods )--------------------------------------------- */ + + + public function canCreatePaymentMethods() { + return true; + } + + + /** + * @phutil-external-symbol class Balanced\Settings + * @phutil-external-symbol class Balanced\Marketplace + * @phutil-external-symbol class RESTful\Exceptions\HTTPError + */ + public function createPaymentMethodFromRequest( + AphrontRequest $request, + PhortunePaymentMethod $method) { + + $card_errors = $request->getStr('cardErrors'); + $balanced_data = $request->getStr('balancedCardData'); + + $errors = array(); + if ($card_errors) { + $raw_errors = json_decode($card_errors); + $errors = $this->parseRawCreatePaymentMethodErrors($raw_errors); + } + + if (!$errors) { + $data = json_decode($balanced_data, true); + if (!is_array($data)) { + $errors[] = pht('An error occurred decoding card data.'); + } + } + + if (!$errors) { + $root = dirname(phutil_get_library_root('phabricator')); + require_once $root.'/externals/httpful/bootstrap.php'; + require_once $root.'/externals/restful/bootstrap.php'; + require_once $root.'/externals/balanced-php/bootstrap.php'; + + $account_phid = $method->getAccountPHID(); + $author_phid = $method->getAuthorPHID(); + $description = $account_phid.':'.$author_phid; + + try { + + Balanced\Settings::$api_key = $this->getSecretKey(); + $buyer = Balanced\Marketplace::mine()->createBuyer( + null, + $data['uri'], + array( + 'description' => $description, + )); + + } catch (RESTful\Exceptions\HTTPError $error) { + // NOTE: This exception doesn't print anything meaningful if it escapes + // to top level. Replace it with something slightly readable. + throw new Exception($error->response->body->description); + } + + $exp_string = $data['expiration_year'].'-'.$data['expiration_month']; + $epoch = strtotime($exp_string); + + $method + ->setName($data['brand'].' / '.$data['last_four']) + ->setExpiresEpoch($epoch) + ->setMetadata( + array( + 'type' => 'balanced.account', + 'balanced.accountURI' => $buyer->uri, + 'balanced.cardURI' => $data['uri'], + )); + } + + return $errors; + } + + public function renderCreatePaymentMethodForm( + AphrontRequest $request, + array $errors) { + + $ccform = id(new PhortuneCreditCardForm()) + ->setUser($request->getUser()) + ->setCardNumberError(isset($errors['number']) ? pht('Invalid') : true) + ->setCardCVCError(isset($errors['cvc']) ? pht('Invalid') : true) + ->setCardExpirationError(isset($errors['exp']) ? pht('Invalid') : null) + ->addScript('https://js.balancedpayments.com/v1/balanced.js'); + + Javelin::initBehavior( + 'balanced-payment-form', + array( + 'balancedMarketplaceURI' => $this->getMarketplaceURI(), + 'formID' => $ccform->getFormID(), + )); + + return $ccform->buildForm(); + } + + private function parseRawCreatePaymentMethodErrors(array $raw_errors) { + $errors = array(); + + foreach ($raw_errors as $error) { + switch ($error) { + case 'number': + $errors[$error] = pht('Card number is incorrect or invalid.'); + break; + case 'cvc': + $errors[$error] = pht('CVC code is incorrect or invalid.'); + break; + case 'exp': + $errors[$error] = pht('Card expiration date is incorrect.'); + break; + default: + $errors[] = $error; + break; + } + } + + return $errors; + } + +} diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php index 5abc9a58a3..0717f3aede 100644 --- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php @@ -59,11 +59,11 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { } private function getPublishableKey() { - return PhabricatorEnv::getEnvConfig('stripe.publishable-key'); + return PhabricatorEnv::getEnvConfig('phortune.stripe.publishable-key'); } private function getSecretKey() { - return PhabricatorEnv::getEnvConfig('stripe.secret-key'); + return PhabricatorEnv::getEnvConfig('phortune.stripe.secret-key'); } @@ -90,11 +90,13 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { 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) { + if (!$stripe_token) { + $errors[] = pht('There was an unknown error processing your card.'); + } + } if (!$errors) { $root = dirname(phutil_get_library_root('phabricator')); @@ -102,6 +104,8 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { try { // First, make sure the token is valid. + $secret_key = $this->getSecretKey(); + $info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key); $account_phid = $method->getAccountPHID(); diff --git a/webroot/rsrc/js/application/phortune/behavior-balanced-payment-form.js b/webroot/rsrc/js/application/phortune/behavior-balanced-payment-form.js new file mode 100644 index 0000000000..2b5c9b25b1 --- /dev/null +++ b/webroot/rsrc/js/application/phortune/behavior-balanced-payment-form.js @@ -0,0 +1,71 @@ +/** + * @provides javelin-behavior-balanced-payment-form + * @requires javelin-behavior + * javelin-dom + * javelin-json + * javelin-workflow + * phortune-credit-card-form + */ + +JX.behavior('balanced-payment-form', function(config) { + balanced.init(config.balancedMarketplaceURI); + + var root = JX.$(config.formID); + var ccform = new JX.PhortuneCreditCardForm(root); + + var onsubmit = function(e) { + e.kill(); + + var cardData = ccform.getCardData(); + var errors = []; + + if (!balanced.card.isCardNumberValid(cardData.number)) { + errors.push('number'); + } + + if (!balanced.card.isSecurityCodeValid(cardData.number, cardData.cvc)) { + errors.push('cvc'); + } + + if (!balanced.card.isExpiryValid(cardData.month, cardData.year)) { + errors.push('expiry'); + } + + if (errors.length) { + JX.Workflow + .newFromForm(root, {cardErrors: JX.JSON.stringify(errors)}) + .start(); + return; + } + + var data = { + card_number: cardData.number, + security_code: cardData.cvc, + expiration_month: cardData.month, + expiration_year: cardData.year + }; + + balanced.card.create(data, onresponse); + } + + var onresponse = function(response) { + + var errors = []; + if (response.error) { + errors = [response.error.type]; + } else if (response.status != 201) { + errors = ['balanced:' + response.status]; + } + + var params = { + cardErrors: JX.JSON.stringify(errors), + balancedCardData: JX.JSON.stringify(response.data) + }; + + JX.Workflow + .newFromForm(root, params) + .start(); + } + + JX.DOM.listen(root, 'submit', null, onsubmit); +}); 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 9715e5f0c7..7c1bbaef82 100644 --- a/webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js +++ b/webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js @@ -59,8 +59,13 @@ JX.behavior('stripe-payment-form', function(config) { token = response.id; } + var params = { + cardErrors: JX.JSON.stringify(errors), + stripeToken: token + }; + JX.Workflow - .newFromForm(root, {cardErrors: errors, stripeToken: token}) + .newFromForm(root, params) .start(); }