diff --git a/externals/wepay/README.md b/externals/wepay/README.md
new file mode 100644
index 0000000000..78a3ccb3d2
--- /dev/null
+++ b/externals/wepay/README.md
@@ -0,0 +1,85 @@
+WePay PHP SDK
+=============
+
+WePay's API allows you to easily add payments into your application.
+
+For full documentation, see [WePay's developer documentation](https://www.wepay.com/developer)
+
+Usage
+-----
+
+In addition to the samples below, we have included a very basic demo application in the `demoapp` directory. See its README file for additional information.
+
+### Configuration ###
+
+For all requests, you must initialize the SDK with your Client ID and Client Secret, into either Staging or Production mode. All API calls made against WePay's staging environment mirror production in functionality, but do not actually move money. This allows you to develop your application and test the checkout experience from the perspective of your users without spending any money on payments. Our [full documentation](https://www.wepay.com/developer) contains additional information on test account numbers you can use in addition to "magic" amounts you can use to trigger payment failures and reversals (helpful for testing IPNs).
+
+**Note:** Staging and Production are two completely independent environments and share NO data with each other. This means that in order to use staging, you must register at [stage.wepay.com](https://stage.wepay.com/developer) and get a set of API keys for your Staging application, and must do the same on Production when you are ready to go live. API keys and access tokens granted on stage *can not* be used on Production, and vice-versa.
+
+ access_token;
+ }
+ else {
+ // Unable to obtain access token
+ }
+ }
+
+Full details on the access token response are [here](https://www.wepay.com/developer/reference/oauth2#token).
+
+**Note:** If you only need access for yourself (e.g., for a personal storefront), the application settings page automatically creates an access token for you. Simply copy and paste it into your code rather than manually going through the authentication flow.
+
+### Making API Calls ###
+
+With the `$access_token` from above, get a new SDK object:
+
+ request('account/find');
+ foreach ($accounts as $account) {
+ echo "account_uri\">$account->name: $account->description
";
+ }
+ }
+ catch (WePayException $e) {
+ // Something went wrong - normally you would log
+ // this and give your user a more informative message
+ echo $e->getMessage();
+ }
+
+And that's it! For more detail on what API calls are available, their parameters and responses, and what permissions they require, please see [our documentation](https://www.wepay.com/developer/reference). For some more detailed examples, look in the `demoapp` directory and check the README. Dropping the entire directory in a web-accessible location and adding your API keys should allow you to be up and running in just a few seconds.
+
+### SSL Certificate ###
+
+If making an API call causes the following problem:
+
+ Uncaught exception 'Exception' with message 'cURL error while making API call to WePay: SSL certificate problem, verify that the CA cert is OK. Details: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed'
+
+You can read the solution here: https://support.wepay.com/entries/21095813-problem-with-ssl-certificate-verification
diff --git a/externals/wepay/demoapp/README b/externals/wepay/demoapp/README
new file mode 100644
index 0000000000..a53835bacc
--- /dev/null
+++ b/externals/wepay/demoapp/README
@@ -0,0 +1,19 @@
+After registering your application at wepay.com (or stage.wepay.com), you
+need to make two updates to this application:
+
+1 - set your client_id and client_secret in _shared.php
+2 - set the redirect_uri in login.php
+
+That should be enough to start making API calls against WePay's API. While
+this is by no means a production-ready example, it should provide you a
+couple ideas on how to get running.
+
+It also defaults to requesting all possible scope fields in the
+authentication request. We suggest limiting the request to the minimum
+your application requires, which will maximize the chance the user
+grants permissions to your application. You can customize this in
+login.php.
+
+If you have any questions, please contact the API team: api@wepay.com
+
+- WePay
diff --git a/externals/wepay/demoapp/_shared.php b/externals/wepay/demoapp/_shared.php
new file mode 100644
index 0000000000..f77892a8cd
--- /dev/null
+++ b/externals/wepay/demoapp/_shared.php
@@ -0,0 +1,4 @@
+
+
WePay Demo App: Account List
+Back
+
+
+request('account/find');
+ foreach ($accounts as $account) {
+ echo "account_uri\">$account->name: $account->description
";
+ }
+}
+catch (WePayException $e) {
+ // Something went wrong - normally you would log
+ // this and give your user a more informative message
+ echo $e->getMessage();
+}
diff --git a/externals/wepay/demoapp/index.php b/externals/wepay/demoapp/index.php
new file mode 100644
index 0000000000..0b44f612ee
--- /dev/null
+++ b/externals/wepay/demoapp/index.php
@@ -0,0 +1,20 @@
+
+
+WePay Demo App
+
+
+Log in with WePay
+
+
+
+User info
+
+Open new account
+
+Account list
+
+Log out
+
+
diff --git a/externals/wepay/demoapp/login.php b/externals/wepay/demoapp/login.php
new file mode 100644
index 0000000000..3f67ad4fda
--- /dev/null
+++ b/externals/wepay/demoapp/login.php
@@ -0,0 +1,41 @@
+access_token;
+ // If desired, you can also store $info->user_id somewhere
+ header('Location: index.php');
+ }
+ else {
+ // Unable to obtain access token
+ echo 'Unable to obtain access token from WePay.';
+ }
+}
diff --git a/externals/wepay/demoapp/logout.php b/externals/wepay/demoapp/logout.php
new file mode 100644
index 0000000000..700adf7968
--- /dev/null
+++ b/externals/wepay/demoapp/logout.php
@@ -0,0 +1,6 @@
+
+WePay Demo App: Open Account
+Back
+
+
+request('account/create', array(
+ 'name' => $name,
+ 'description' => $desc,
+ ));
+ echo "Created account $name for '$desc'! View on WePay at account_uri\">$account->account_uri. See all of your accounts here.";
+ }
+ catch (WePayException $e) {
+ // Something went wrong - normally you would log
+ // this and give your user a more informative message
+ echo $e->getMessage();
+ }
+ }
+ else {
+ echo 'Account name and description are both required.';
+ }
+}
+?>
+
+
diff --git a/externals/wepay/demoapp/user.php b/externals/wepay/demoapp/user.php
new file mode 100644
index 0000000000..fccb8ed06d
--- /dev/null
+++ b/externals/wepay/demoapp/user.php
@@ -0,0 +1,22 @@
+
+WePay Demo App: User Info
+Back
+
+
+request('user');
+ echo '';
+ foreach ($user as $key => $value) {
+ echo "- $key
- $value
";
+ }
+ echo '
';
+}
+catch (WePayException $e) {
+ // Something went wrong - normally you would log
+ // this and give your user a more informative message
+ echo $e->getMessage();
+}
diff --git a/externals/wepay/iframe_demoapp/checkout.php b/externals/wepay/iframe_demoapp/checkout.php
new file mode 100755
index 0000000000..06fb4ee369
--- /dev/null
+++ b/externals/wepay/iframe_demoapp/checkout.php
@@ -0,0 +1,69 @@
+request('/checkout/create', array(
+ 'account_id' => $account_id, // ID of the account that you want the money to go to
+ 'amount' => 100, // dollar amount you want to charge the user
+ 'short_description' => "this is a test payment", // a short description of what the payment is for
+ 'type' => "GOODS", // the type of the payment - choose from GOODS SERVICE DONATION or PERSONAL
+ 'mode' => "iframe", // put iframe here if you want the checkout to be in an iframe, regular if you want the user to be sent to WePay
+ )
+ );
+} catch (WePayException $e) { // if the API call returns an error, get the error message for display later
+ $error = $e->getMessage();
+}
+
+?>
+
+
+
+
+
+
+
+ Checkout:
+
+ The user will checkout here:
+
+
+ ERROR:
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/externals/wepay/iframe_demoapp/list_accounts.php b/externals/wepay/iframe_demoapp/list_accounts.php
new file mode 100755
index 0000000000..d34ecae01f
--- /dev/null
+++ b/externals/wepay/iframe_demoapp/list_accounts.php
@@ -0,0 +1,74 @@
+request('/account/find');
+} catch (WePayException $e) { // if the API call returns an error, get the error message for display later
+ $error = $e->getMessage();
+}
+
+?>
+
+
+
+
+
+
+
+ List all accounts:
+
+ The following is a list of all accounts that this user owns
+
+
+ ERROR:
+
+ You do not have any accounts. Go to https://stage.wepay.com to open an account.
+
+
+
+
+ Account ID |
+ Account Name |
+ Account Description |
+
+
+
+
+
+ account_id ?> |
+ name ?> |
+ description ?> |
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/externals/wepay/wepay.php b/externals/wepay/wepay.php
new file mode 100755
index 0000000000..f0b425f344
--- /dev/null
+++ b/externals/wepay/wepay.php
@@ -0,0 +1,294 @@
+ self::$client_id,
+ 'redirect_uri' => $redirect_uri,
+ 'scope' => implode(',', $scope),
+ 'state' => empty($options['state']) ? '' : $options['state'],
+ 'user_name' => empty($options['user_name']) ? '' : $options['user_name'],
+ 'user_email' => empty($options['user_email']) ? '' : $options['user_email'],
+ ), '', '&');
+ return $uri;
+ }
+
+ private static function getDomain() {
+ if (self::$production === true) {
+ return 'https://wepayapi.com/v2/';
+ }
+ elseif (self::$production === false) {
+ return 'https://stage.wepayapi.com/v2/';
+ }
+ else {
+ throw new RuntimeException('You must initialize the WePay SDK with WePay::useStaging() or WePay::useProduction()');
+ }
+ }
+
+ /**
+ * Exchange a temporary access code for a (semi-)permanent access token
+ * @param string $code 'code' field from query string passed to your redirect_uri page
+ * @param string $redirect_uri Where user went after logging in at WePay (must match value from getAuthorizationUri)
+ * @return StdClass|false
+ * user_id
+ * access_token
+ * token_type
+ */
+ public static function getToken($code, $redirect_uri) {
+ $params = (array(
+ 'client_id' => self::$client_id,
+ 'client_secret' => self::$client_secret,
+ 'redirect_uri' => $redirect_uri,
+ 'code' => $code,
+ 'state' => '', // do not hardcode
+ ));
+ $result = self::make_request('oauth2/token', $params);
+ return $result;
+ }
+
+ /**
+ * Configure SDK to run against WePay's production servers
+ * @param string $client_id Your application's client id
+ * @param string $client_secret Your application's client secret
+ * @return void
+ * @throws RuntimeException
+ */
+ public static function useProduction($client_id, $client_secret) {
+ if (self::$production !== null) {
+ throw new RuntimeException('API mode has already been set.');
+ }
+ self::$production = true;
+ self::$client_id = $client_id;
+ self::$client_secret = $client_secret;
+ }
+
+ /**
+ * Configure SDK to run against WePay's staging servers
+ * @param string $client_id Your application's client id
+ * @param string $client_secret Your application's client secret
+ * @return void
+ * @throws RuntimeException
+ */
+ public static function useStaging($client_id, $client_secret) {
+ if (self::$production !== null) {
+ throw new RuntimeException('API mode has already been set.');
+ }
+ self::$production = false;
+ self::$client_id = $client_id;
+ self::$client_secret = $client_secret;
+ }
+
+ /**
+ * Create a new API session
+ * @param string $token - access_token returned from WePay::getToken
+ */
+ public function __construct($token) {
+ if ($token && !is_string($token)) {
+ throw new InvalidArgumentException('$token must be a string, ' . gettype($token) . ' provided');
+ }
+ $this->token = $token;
+ }
+
+ /**
+ * Clean up cURL handle
+ */
+ public function __destruct() {
+ if (self::$ch) {
+ curl_close(self::$ch);
+ self::$ch = NULL;
+ }
+ }
+
+ /**
+ * create the cURL request and execute it
+ */
+ private static function make_request($endpoint, $values, $headers = array())
+ {
+ self::$ch = curl_init();
+ $headers = array_merge(array("Content-Type: application/json"), $headers); // always pass the correct Content-Type header
+ curl_setopt(self::$ch, CURLOPT_USERAGENT, 'WePay v2 PHP SDK v' . self::VERSION);
+ curl_setopt(self::$ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt(self::$ch, CURLOPT_HTTPHEADER, $headers);
+ curl_setopt(self::$ch, CURLOPT_TIMEOUT, 30); // 30-second timeout, adjust to taste
+ curl_setopt(self::$ch, CURLOPT_POST, !empty($values)); // WePay's API is not strictly RESTful, so all requests are sent as POST unless there are no request values
+
+ $uri = self::getDomain() . $endpoint;
+ curl_setopt(self::$ch, CURLOPT_URL, $uri);
+
+ if (!empty($values)) {
+ curl_setopt(self::$ch, CURLOPT_POSTFIELDS, json_encode($values));
+ }
+
+ $raw = curl_exec(self::$ch);
+ if ($errno = curl_errno(self::$ch)) {
+ // Set up special handling for request timeouts
+ if ($errno == CURLE_OPERATION_TIMEOUTED) {
+ throw new WePayServerException("Timeout occurred while trying to connect to WePay");
+ }
+ throw new Exception('cURL error while making API call to WePay: ' . curl_error(self::$ch), $errno);
+ }
+ $result = json_decode($raw);
+
+ $error_code = null;
+ if (isset($result->error_code)) {
+ $error_code = $result->error_code;
+ }
+
+ $httpCode = curl_getinfo(self::$ch, CURLINFO_HTTP_CODE);
+ if ($httpCode >= 400) {
+ if ($httpCode >= 500) {
+ throw new WePayServerException($result->error_description, $httpCode, $result, $error_code);
+ }
+ switch ($result->error) {
+ case 'invalid_request':
+ throw new WePayRequestException($result->error_description, $httpCode, $result, $error_code);
+ case 'access_denied':
+ default:
+ throw new WePayPermissionException($result->error_description, $httpCode, $result, $error_code);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Make API calls against authenticated user
+ * @param string $endpoint - API call to make (ex. 'user', 'account/find')
+ * @param array $values - Associative array of values to send in API call
+ * @return StdClass
+ * @throws WePayException on failure
+ * @throws Exception on catastrophic failure (non-WePay-specific cURL errors)
+ */
+ public function request($endpoint, array $values = array()) {
+ $headers = array();
+
+ if ($this->token) { // if we have an access_token, add it to the Authorization header
+ $headers[] = "Authorization: Bearer $this->token";
+ }
+
+ $result = self::make_request($endpoint, $values, $headers);
+
+ return $result;
+ }
+}
+
+/**
+ * Different problems will have different exception types so you can
+ * catch and handle them differently.
+ *
+ * WePayServerException indicates some sort of 500-level error code and
+ * was unavoidable from your perspective. You may need to re-run the
+ * call, or check whether it was received (use a "find" call with your
+ * reference_id and make a decision based on the response)
+ *
+ * WePayRequestException indicates a development error - invalid endpoint,
+ * erroneous parameter, etc.
+ *
+ * WePayPermissionException indicates your authorization token has expired,
+ * was revoked, or is lacking in scope for the call you made
+ */
+class WePayException extends Exception {
+ public function __construct($description = '', $http_code = FALSE, $response = FALSE, $code = 0, $previous = NULL)
+ {
+ $this->response = $response;
+
+ if (!defined('PHP_VERSION_ID')) {
+ $version = explode('.', PHP_VERSION);
+ define('PHP_VERSION_ID', ($version[0] * 10000 + $version[1] * 100 + $version[2]));
+ }
+
+ if (PHP_VERSION_ID < 50300) {
+ parent::__construct($description, $code);
+ } else {
+ parent::__construct($description, $code, $previous);
+ }
+ }
+}
+class WePayRequestException extends WePayException {}
+class WePayPermissionException extends WePayException {}
+class WePayServerException extends WePayException {}
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 4720fede74..859bcf7f6c 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1649,6 +1649,7 @@ phutil_register_library_map(array(
'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
'PhortuneTestExtraPaymentProvider' => 'applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php',
'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
+ 'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php',
'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php',
'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php',
@@ -3426,6 +3427,7 @@ phutil_register_library_map(array(
'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider',
'PhortuneTestExtraPaymentProvider' => 'PhortunePaymentProvider',
'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
+ 'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
'PhrequentController' => 'PhabricatorController',
'PhrequentDAO' => 'PhabricatorLiskDAO',
'PhrequentListController' => 'PhrequentController',
diff --git a/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php b/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php
index 6910e0b890..fcea8e3746 100644
--- a/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php
+++ b/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php
@@ -51,6 +51,18 @@ final class PhabricatorPhortuneConfigOptions
->setHidden(true)
->setDescription(
pht('PayPal API signature.')),
+ $this->newOption('phortune.wepay.client-id', 'string', null)
+ ->setLocked(true)
+ ->setDescription(pht('WePay application ID.')),
+ $this->newOption('phortune.wepay.client-secret', 'string', null)
+ ->setHidden(true)
+ ->setDescription(pht('WePay application secret.')),
+ $this->newOption('phortune.wepay.access-token', 'string', null)
+ ->setHidden(true)
+ ->setDescription(pht('WePay access token.')),
+ $this->newOption('phortune.wepay.account-id', 'string', null)
+ ->setHidden(true)
+ ->setDescription(pht('WePay account ID.')),
);
}
diff --git a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
new file mode 100644
index 0000000000..927feeba52
--- /dev/null
+++ b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
@@ -0,0 +1,187 @@
+getWePayClientID() &&
+ $this->getWePayClientSecret() &&
+ $this->getWePayAccessToken() &&
+ $this->getWePayAccountID();
+ }
+
+ public function getProviderType() {
+ return 'wepay';
+ }
+
+ public function getProviderDomain() {
+ return 'wepay.com';
+ }
+
+ public function getPaymentMethodDescription() {
+ return pht('Credit Card or Bank Account');
+ }
+
+ public function getPaymentMethodIcon() {
+ return 'rsrc/phortune/wepay.png';
+ }
+
+ public function getPaymentMethodProviderDescription() {
+ return "WePay";
+ }
+
+
+ public function canHandlePaymentMethod(PhortunePaymentMethod $method) {
+ $type = $method->getMetadataValue('type');
+ return ($type == 'wepay');
+ }
+
+ protected function executeCharge(
+ PhortunePaymentMethod $payment_method,
+ PhortuneCharge $charge) {
+ throw new Exception("!");
+ }
+
+ private function getWePayClientID() {
+ return PhabricatorEnv::getEnvConfig('phortune.wepay.client-id');
+ }
+
+ private function getWePayClientSecret() {
+ return PhabricatorEnv::getEnvConfig('phortune.wepay.client-secret');
+ }
+
+ private function getWePayAccessToken() {
+ return PhabricatorEnv::getEnvConfig('phortune.wepay.access-token');
+ }
+
+ private function getWePayAccountID() {
+ return PhabricatorEnv::getEnvConfig('phortune.wepay.account-id');
+ }
+
+
+/* -( One-Time Payments )-------------------------------------------------- */
+
+ public function canProcessOneTimePayments() {
+ return true;
+ }
+
+ public function renderOneTimePaymentButton(
+ PhortuneAccount $account,
+ PhortuneCart $cart,
+ PhabricatorUser $user) {
+
+ $uri = $this->getControllerURI(
+ 'checkout',
+ array(
+ 'cartID' => $cart->getID(),
+ ));
+
+ return phabricator_form(
+ $user,
+ array(
+ 'action' => $uri,
+ 'method' => 'POST',
+ ),
+ phutil_tag(
+ 'button',
+ array(
+ 'class' => 'green',
+ 'type' => 'submit',
+ ),
+ pht('Pay with WePay')));
+ }
+
+
+/* -( Controllers )-------------------------------------------------------- */
+
+
+ public function canRespondToControllerAction($action) {
+ switch ($action) {
+ case 'checkout':
+ case 'charge':
+ case 'cancel':
+ return true;
+ }
+ return parent::canRespondToControllerAction();
+ }
+
+ /**
+ * @phutil-external-symbol class WePay
+ */
+ public function processControllerRequest(
+ PhortuneProviderController $controller,
+ AphrontRequest $request) {
+
+ $cart = $controller->loadCart($request->getInt('cartID'));
+ if (!$cart) {
+ return new Aphront404Response();
+ }
+
+ $root = dirname(phutil_get_library_root('phabricator'));
+ require_once $root.'/externals/wepay/wepay.php';
+
+ WePay::useStaging(
+ $this->getWePayClientID(),
+ $this->getWePayClientSecret());
+
+ $wepay = new WePay($this->getWePayAccessToken());
+
+ switch ($controller->getAction()) {
+ case 'checkout':
+ $return_uri = $this->getControllerURI(
+ 'charge',
+ array(
+ 'cartID' => $cart->getID(),
+ ));
+
+ $cancel_uri = $this->getControllerURI(
+ 'cancel',
+ array(
+ 'cartID' => $cart->getID(),
+ ));
+
+ $total_in_cents = $cart->getTotalInCents();
+ $price = PhortuneCurrency::newFromUSDCents($total_in_cents);
+
+ $params = array(
+ 'account_id' => $this->getWePayAccountID(),
+ 'short_description' => 'Services', // TODO
+ 'type' => 'SERVICE',
+ 'amount' => $price->formatBareValue(),
+ 'long_description' => 'Services', // TODO
+ 'reference_id' => $cart->getPHID(),
+ 'app_fee' => 0,
+ 'fee_payer' => 'Payee',
+ 'redirect_uri' => $return_uri,
+ 'fallback_uri' => $cancel_uri,
+ 'auto_capture' => false,
+ 'require_shipping' => 0,
+ 'shipping_fee' => 0,
+ 'charge_tax' => 0,
+ 'mode' => 'regular',
+ 'funding_sources' => 'bank,cc'
+ );
+
+ $result = $wepay->request('checkout/create', $params);
+
+ // NOTE: We might want to store "$result->checkout_id" on the Cart.
+
+ $uri = new PhutilURI($result->checkout_uri);
+ return id(new AphrontRedirectResponse())->setURI($uri);
+ case 'charge':
+
+ // NOTE: We get $_REQUEST['checkout_id'] here, but our parameters are
+ // dropped so we should stop depending on them or shove them into the
+ // URI.
+
+ var_dump($_REQUEST);
+ break;
+ case 'cancel':
+ var_dump($_REQUEST);
+ break;
+ }
+
+ throw new Exception("The rest of this isn't implemented yet.");
+ }
+
+
+}